From 5145121eb7438394667da358553977d6997a1142 Mon Sep 17 00:00:00 2001 From: Danny Avila <110412045+danny-avila@users.noreply.github.com> Date: Wed, 11 Oct 2023 17:05:47 -0400 Subject: [PATCH] feat(api): initial Redis support; fix(SearchBar): proper debounce (#1039) * refactor: use keyv for search caching with 1 min expirations * feat: keyvRedis; chore: bump keyv, bun.lockb, add jsconfig for vscode file resolution * feat: api/search redis support * refactor(redis) use ioredis cluster for keyv fix(OpenID): when redis is configured, use redis memory store for express-session * fix: revert using uri for keyvredis * fix(SearchBar): properly debounce search queries, fix weird render behaviors * refactor: add authentication to search endpoint and show error messages in results * feat: redis support for violation logs * fix(logViolation): ensure a number is always being stored in cache * feat(concurrentLimiter): uses clearPendingReq, clears pendingReq on abort, redis support * fix(api/search/enable): query only when authenticated * feat(ModelService): redis support * feat(checkBan): redis support * refactor(api/search): consolidate keyv logic * fix(ci): add default empty value for REDIS_URI * refactor(keyvRedis): use condition to initialize keyvRedis assignment * refactor(connectDb): handle disconnected state (should create a new conn) * fix(ci/e2e): handle case where cleanUp did not successfully run * fix(getDefaultEndpoint): return endpoint from localStorage if defined and endpointsConfig is default * ci(e2e): remove afterAll messages as startup/cleanUp will clear messages * ci(e2e): remove teardown for CI until further notice * chore: bump playwright/test * ci(e2e): reinstate teardown as CI issue is specific to github env * fix(ci): click settings menu trigger by testid --- api/cache/clearPendingReq.js | 45 +++- api/cache/getLogStores.js | 40 +-- api/cache/index.js | 3 +- api/cache/keyvRedis.js | 14 ++ api/cache/logViolation.js | 12 +- api/cache/redis.js | 4 + api/jsconfig.json | 13 + api/lib/db/connectDb.js | 5 +- api/package.json | 5 +- api/server/middleware/abortMiddleware.js | 6 +- api/server/middleware/checkBan.js | 19 +- api/server/middleware/concurrentLimiter.js | 46 ++-- api/server/routes/search.js | 28 ++- api/server/services/ModelService.js | 6 +- api/server/socialLogins.js | 18 +- bun.lockb | Bin 755985 -> 760818 bytes .../src/components/Messages/Content/Error.tsx | 1 - client/src/components/Messages/Message.tsx | 2 +- client/src/components/Nav/Nav.tsx | 11 +- client/src/components/Nav/NavLinks.tsx | 1 + client/src/components/Nav/SearchBar.tsx | 42 ++-- client/src/routes/Root.tsx | 2 +- client/src/store/endpoints.ts | 21 +- client/src/utils/getDefaultEndpoint.ts | 15 +- e2e/setup/authenticate.ts | 23 +- e2e/specs/messages.spec.ts | 8 - e2e/specs/nav.spec.ts | 8 +- package-lock.json | 232 ++++++++++++++++-- package.json | 2 +- 29 files changed, 461 insertions(+), 171 deletions(-) create mode 100644 api/cache/keyvRedis.js create mode 100644 api/cache/redis.js create mode 100644 api/jsconfig.json diff --git a/api/cache/clearPendingReq.js b/api/cache/clearPendingReq.js index d31d51d78..068711d31 100644 --- a/api/cache/clearPendingReq.js +++ b/api/cache/clearPendingReq.js @@ -1,29 +1,48 @@ -const Keyv = require('keyv'); -const { pendingReqFile } = require('./keyvFiles'); -const { LIMIT_CONCURRENT_MESSAGES } = process.env ?? {}; - -const keyv = new Keyv({ store: pendingReqFile, namespace: 'pendingRequests' }); +const getLogStores = require('./getLogStores'); +const { isEnabled } = require('../server/utils'); +const { USE_REDIS, LIMIT_CONCURRENT_MESSAGES } = process.env ?? {}; +const ttl = 1000 * 60 * 1; /** - * Clear pending requests from the cache. + * Clear or decrement pending requests from the cache. * Checks the environmental variable LIMIT_CONCURRENT_MESSAGES; - * if the rule is enabled ('true'), pending requests in the cache are cleared. + * 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 keyv - * @requires keyvFiles + * @requires ./getLogStores + * @requires ../server/utils * @requires process * * @async * @function - * @returns {Promise} A promise that either clears 'pendingRequests' from store or resolves with no value. + * @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} A promise that either decrements the 'pendingRequests' count, deletes the key from the store, or resolves with no value. */ -const clearPendingReq = async () => { - if (LIMIT_CONCURRENT_MESSAGES?.toLowerCase() !== 'true') { +const clearPendingReq = async ({ userId, cache: _cache }) => { + if (!userId) { + return; + } else if (!isEnabled(LIMIT_CONCURRENT_MESSAGES)) { return; } - await keyv.clear(); + const namespace = 'pending_req'; + const cache = _cache ?? getLogStores(namespace); + + if (!cache) { + return; + } + + const key = `${USE_REDIS ? namespace : ''}:${userId ?? ''}`; + const currentReq = +((await cache.get(key)) ?? 0); + + if (currentReq && currentReq >= 1) { + await cache.set(key, currentReq - 1, ttl); + } else { + await cache.delete(key); + } }; module.exports = clearPendingReq; diff --git a/api/cache/getLogStores.js b/api/cache/getLogStores.js index 56839fcd2..2692b6933 100644 --- a/api/cache/getLogStores.js +++ b/api/cache/getLogStores.js @@ -1,26 +1,37 @@ const Keyv = require('keyv'); const keyvMongo = require('./keyvMongo'); -const { math } = require('../server/utils'); +const keyvRedis = require('./keyvRedis'); +const { math, isEnabled } = require('../server/utils'); const { logFile, violationFile } = require('./keyvFiles'); -const { BAN_DURATION } = process.env ?? {}; +const { BAN_DURATION, USE_REDIS } = process.env ?? {}; const duration = math(BAN_DURATION, 7200000); +const createViolationInstance = (namespace) => { + const config = isEnabled(USE_REDIS) ? { store: keyvRedis } : { store: violationFile, namespace }; + return new Keyv(config); +}; + +// Serve cache from memory so no need to clear it on startup/exit +const pending_req = isEnabled(USE_REDIS) + ? new Keyv({ store: keyvRedis }) + : new Keyv({ namespace: 'pending_req' }); + const namespaces = { - ban: new Keyv({ store: keyvMongo, ttl: duration, namespace: 'bans' }), + pending_req, + ban: new Keyv({ store: keyvMongo, namespace: 'bans', duration }), general: new Keyv({ store: logFile, namespace: 'violations' }), - concurrent: new Keyv({ store: violationFile, namespace: 'concurrent' }), - non_browser: new Keyv({ store: violationFile, namespace: 'non_browser' }), - message_limit: new Keyv({ store: violationFile, namespace: 'message_limit' }), - token_balance: new Keyv({ store: violationFile, namespace: 'token_balance' }), - registrations: new Keyv({ store: violationFile, namespace: 'registrations' }), - logins: new Keyv({ store: violationFile, namespace: 'logins' }), + concurrent: createViolationInstance('concurrent'), + non_browser: createViolationInstance('non_browser'), + message_limit: createViolationInstance('message_limit'), + token_balance: createViolationInstance('token_balance'), + registrations: createViolationInstance('registrations'), + logins: createViolationInstance('logins'), }; /** - * Returns either the logs of violations specified by type if a type is provided - * or it returns the general log if no type is specified. If an invalid type is passed, - * an error will be thrown. + * Returns the keyv cache specified by type. + * If an invalid type is passed, an error will be thrown. * * @module getLogStores * @requires keyv - a simple key-value storage that allows you to easily switch out storage adapters. @@ -31,11 +42,10 @@ const namespaces = { * @throws Will throw an error if an invalid violation type is passed. */ const getLogStores = (type) => { - if (!type) { + if (!type || !namespaces[type]) { throw new Error(`Invalid store type: ${type}`); } - const logs = namespaces[type]; - return logs; + return namespaces[type]; }; module.exports = getLogStores; diff --git a/api/cache/index.js b/api/cache/index.js index 1edbf981d..bb1e77418 100644 --- a/api/cache/index.js +++ b/api/cache/index.js @@ -1,6 +1,5 @@ const keyvFiles = require('./keyvFiles'); const getLogStores = require('./getLogStores'); const logViolation = require('./logViolation'); -const clearPendingReq = require('./clearPendingReq'); -module.exports = { ...keyvFiles, getLogStores, logViolation, clearPendingReq }; +module.exports = { ...keyvFiles, getLogStores, logViolation }; diff --git a/api/cache/keyvRedis.js b/api/cache/keyvRedis.js new file mode 100644 index 000000000..942b1b239 --- /dev/null +++ b/api/cache/keyvRedis.js @@ -0,0 +1,14 @@ +const KeyvRedis = require('@keyv/redis'); + +const { REDIS_URI } = process.env; + +let keyvRedis; + +if (REDIS_URI) { + keyvRedis = new KeyvRedis(REDIS_URI, { useRedisSets: false }); + keyvRedis.on('error', (err) => console.error('KeyvRedis connection error:', err)); +} else { + // console.log('REDIS_URI not provided. Redis module will not be initialized.'); +} + +module.exports = keyvRedis; diff --git a/api/cache/logViolation.js b/api/cache/logViolation.js index 9f045421a..7fe85afd8 100644 --- a/api/cache/logViolation.js +++ b/api/cache/logViolation.js @@ -1,5 +1,6 @@ const getLogStores = require('./getLogStores'); const banViolation = require('./banViolation'); +const { isEnabled } = require('../server/utils'); /** * Logs the violation. @@ -17,10 +18,11 @@ const logViolation = async (req, res, type, errorMessage, score = 1) => { } const logs = getLogStores('general'); const violationLogs = getLogStores(type); + const key = isEnabled(process.env.USE_REDIS) ? `${type}:${userId}` : userId; - const userViolations = (await violationLogs.get(userId)) ?? 0; - const violationCount = userViolations + score; - await violationLogs.set(userId, violationCount); + const userViolations = (await violationLogs.get(key)) ?? 0; + const violationCount = +userViolations + +score; + await violationLogs.set(key, violationCount); errorMessage.user_id = userId; errorMessage.prev_count = userViolations; @@ -28,10 +30,10 @@ const logViolation = async (req, res, type, errorMessage, score = 1) => { errorMessage.date = new Date().toISOString(); await banViolation(req, res, errorMessage); - const userLogs = (await logs.get(userId)) ?? []; + const userLogs = (await logs.get(key)) ?? []; userLogs.push(errorMessage); delete errorMessage.user_id; - await logs.set(userId, userLogs); + await logs.set(key, userLogs); }; module.exports = logViolation; diff --git a/api/cache/redis.js b/api/cache/redis.js new file mode 100644 index 000000000..adf291d02 --- /dev/null +++ b/api/cache/redis.js @@ -0,0 +1,4 @@ +const Redis = require('ioredis'); +const { REDIS_URI } = process.env ?? {}; +const redis = new Redis.Cluster(REDIS_URI); +module.exports = redis; diff --git a/api/jsconfig.json b/api/jsconfig.json new file mode 100644 index 000000000..756746fbf --- /dev/null +++ b/api/jsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES6", + "module": "CommonJS", + // "checkJs": true, // Report errors in JavaScript files + "baseUrl": "./", + "paths": { + "*": ["*", "node_modules/*"], + "~/*": ["./*"] + } + }, + "exclude": ["node_modules"] +} diff --git a/api/lib/db/connectDb.js b/api/lib/db/connectDb.js index 8b9cdae01..3e711ca7a 100644 --- a/api/lib/db/connectDb.js +++ b/api/lib/db/connectDb.js @@ -18,11 +18,12 @@ if (!cached) { } async function connectDb() { - if (cached.conn) { + if (cached.conn && cached.conn?._readyState === 1) { return cached.conn; } - if (!cached.promise) { + const disconnected = cached.conn && cached.conn?._readyState !== 1; + if (!cached.promise || disconnected) { const opts = { useNewUrlParser: true, useUnifiedTopology: true, diff --git a/api/package.json b/api/package.json index f01b8c55c..4453c9c26 100644 --- a/api/package.json +++ b/api/package.json @@ -24,11 +24,13 @@ "@anthropic-ai/sdk": "^0.5.4", "@azure/search-documents": "^11.3.2", "@keyv/mongo": "^2.1.8", + "@keyv/redis": "^2.8.0", "@waylaidwanderer/chatgpt-api": "^1.37.2", "axios": "^1.3.4", "bcryptjs": "^2.4.3", "cheerio": "^1.0.0-rc.12", "cohere-ai": "^6.0.0", + "connect-redis": "^7.1.0", "cookie": "^0.5.0", "cors": "^2.8.5", "dotenv": "^16.0.3", @@ -39,10 +41,11 @@ "googleapis": "^118.0.0", "handlebars": "^4.7.7", "html": "^1.0.0", + "ioredis": "^5.3.2", "jose": "^4.15.2", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.0", - "keyv": "^4.5.3", + "keyv": "^4.5.4", "keyv-file": "^0.2.0", "langchain": "^0.0.153", "lodash": "^4.17.21", diff --git a/api/server/middleware/abortMiddleware.js b/api/server/middleware/abortMiddleware.js index fc9a44155..a65d09c88 100644 --- a/api/server/middleware/abortMiddleware.js +++ b/api/server/middleware/abortMiddleware.js @@ -1,5 +1,6 @@ +const { sendMessage, sendError, countTokens, isEnabled } = require('../utils'); const { saveMessage, getConvo, getConvoTitle } = require('../../models'); -const { sendMessage, sendError, countTokens } = require('../utils'); +const clearPendingReq = require('../../cache/clearPendingReq'); const spendTokens = require('../../models/spendTokens'); const abortControllers = require('./abortControllers'); @@ -20,6 +21,9 @@ async function abortMessage(req, res) { const handleAbort = () => { return async (req, res) => { try { + if (isEnabled(process.env.LIMIT_CONCURRENT_MESSAGES)) { + await clearPendingReq({ userId: req.user.id }); + } return await abortMessage(req, res); } catch (err) { console.error(err); diff --git a/api/server/middleware/checkBan.js b/api/server/middleware/checkBan.js index 294f4a668..c744dda07 100644 --- a/api/server/middleware/checkBan.js +++ b/api/server/middleware/checkBan.js @@ -3,8 +3,11 @@ const uap = require('ua-parser-js'); const { getLogStores } = require('../../cache'); const denyRequest = require('./denyRequest'); const { isEnabled, removePorts } = require('../utils'); +const keyvRedis = require('../../cache/keyvRedis'); -const banCache = new Keyv({ namespace: 'bans', ttl: 0 }); +const banCache = isEnabled(process.env.USE_REDIS) + ? new Keyv({ store: keyvRedis }) + : new Keyv({ namespace: 'bans', ttl: 0 }); const message = 'Your account has been temporarily banned due to violations of our service.'; /** @@ -50,9 +53,11 @@ const checkBan = async (req, res, next = () => {}) => { req.ip = removePorts(req); const userId = req.user?.id ?? req.user?._id ?? null; + const ipKey = isEnabled(process.env.USE_REDIS) ? `ban_cache:ip:${req.ip}` : req.ip; + const userKey = isEnabled(process.env.USE_REDIS) ? `ban_cache:user:${userId}` : userId; - const cachedIPBan = await banCache.get(req.ip); - const cachedUserBan = await banCache.get(userId); + const cachedIPBan = await banCache.get(ipKey); + const cachedUserBan = await banCache.get(userKey); const cachedBan = cachedIPBan || cachedUserBan; if (cachedBan) { @@ -78,13 +83,13 @@ const checkBan = async (req, res, next = () => {}) => { const timeLeft = Number(isBanned.expiresAt) - Date.now(); if (timeLeft <= 0) { - await banLogs.delete(req.ip); - await banLogs.delete(userId); + await banLogs.delete(ipKey); + await banLogs.delete(userKey); return next(); } - banCache.set(req.ip, isBanned, timeLeft); - banCache.set(userId, isBanned, timeLeft); + banCache.set(ipKey, isBanned, timeLeft); + banCache.set(userKey, isBanned, timeLeft); req.banned = true; return await banResponse(req, res); }; diff --git a/api/server/middleware/concurrentLimiter.js b/api/server/middleware/concurrentLimiter.js index d110b1b86..402152eb0 100644 --- a/api/server/middleware/concurrentLimiter.js +++ b/api/server/middleware/concurrentLimiter.js @@ -1,10 +1,13 @@ -const Keyv = require('keyv'); -const { logViolation } = require('../../cache'); - +const clearPendingReq = require('../../cache/clearPendingReq'); +const { logViolation, getLogStores } = require('../../cache'); const denyRequest = require('./denyRequest'); -// Serve cache from memory so no need to clear it on startup/exit -const pendingReqCache = new Keyv({ namespace: 'pendingRequests' }); +const { + USE_REDIS, + CONCURRENT_MESSAGE_MAX = 1, + CONCURRENT_VIOLATION_SCORE: score, +} = process.env ?? {}; +const ttl = 1000 * 60 * 1; /** * Middleware to limit concurrent requests for a user. @@ -12,7 +15,7 @@ const pendingReqCache = new Keyv({ namespace: 'pendingRequests' }); * This middleware checks if a user has exceeded a specified concurrent request limit. * If the user exceeds the limit, an error is returned. If the user is within the limit, * their request count is incremented. After the request is processed, the count is decremented. - * If the `pendingReqCache` store is not available, the middleware will skip its logic. + * If the `cache` store is not available, the middleware will skip its logic. * * @function * @param {Object} req - Express request object containing user information. @@ -21,7 +24,9 @@ const pendingReqCache = new Keyv({ namespace: 'pendingRequests' }); * @throws {Error} Throws an error if the user exceeds the concurrent request limit. */ const concurrentLimiter = async (req, res, next) => { - if (!pendingReqCache) { + const namespace = 'pending_req'; + const cache = getLogStores(namespace); + if (!cache) { return next(); } @@ -29,12 +34,12 @@ const concurrentLimiter = async (req, res, next) => { return next(); } - const { CONCURRENT_MESSAGE_MAX = 1, CONCURRENT_VIOLATION_SCORE: score } = process.env; + const userId = req.user?.id ?? req.user?._id ?? ''; const limit = Math.max(CONCURRENT_MESSAGE_MAX, 1); const type = 'concurrent'; - const userId = req.user?.id ?? req.user?._id ?? null; - const pendingRequests = (await pendingReqCache.get(userId)) ?? 0; + const key = `${USE_REDIS ? namespace : ''}:${userId}`; + const pendingRequests = +((await cache.get(key)) ?? 0); if (pendingRequests >= limit) { const errorMessage = { @@ -46,22 +51,17 @@ const concurrentLimiter = async (req, res, next) => { await logViolation(req, res, type, errorMessage, score); return await denyRequest(req, res, errorMessage); } else { - await pendingReqCache.set(userId, pendingRequests + 1); + await cache.set(key, pendingRequests + 1, ttl); } // Ensure the requests are removed from the store once the request is done + let cleared = false; const cleanUp = async () => { - if (!pendingReqCache) { + if (cleared) { return; } - - const currentRequests = await pendingReqCache.get(userId); - - if (currentRequests && currentRequests >= 1) { - await pendingReqCache.set(userId, currentRequests - 1); - } else { - await pendingReqCache.delete(userId); - } + cleared = true; + await clearPendingReq({ userId, cache }); }; if (pendingRequests < limit) { @@ -72,10 +72,4 @@ const concurrentLimiter = async (req, res, next) => { next(); }; -// if cache is not served from memory, clear it on exit -// process.on('exit', async () => { -// console.log('Clearing all pending requests before exiting...'); -// await pendingReqCache.clear(); -// }); - module.exports = concurrentLimiter; diff --git a/api/server/routes/search.js b/api/server/routes/search.js index 955e58da9..98720a2ae 100644 --- a/api/server/routes/search.js +++ b/api/server/routes/search.js @@ -1,3 +1,4 @@ +const Keyv = require('keyv'); const express = require('express'); const router = express.Router(); const { MeiliSearch } = require('meilisearch'); @@ -6,8 +7,15 @@ const { Conversation, getConvosQueried } = require('../../models/Conversation'); const { reduceHits } = require('../../lib/utils/reduceHits'); const { cleanUpPrimaryKeyValue } = require('../../lib/utils/misc'); const requireJwtAuth = require('../middleware/requireJwtAuth'); +const keyvRedis = require('../../cache/keyvRedis'); +const { isEnabled } = require('../utils'); -const cache = new Map(); +const expiration = 60 * 1000; +const cache = isEnabled(process.env.USE_REDIS) + ? new Keyv({ store: keyvRedis }) + : new Keyv({ namespace: 'search', ttl: expiration }); + +router.use(requireJwtAuth); router.get('/sync', async function (req, res) { await Message.syncWithMeili(); @@ -15,24 +23,20 @@ router.get('/sync', async function (req, res) { res.send('synced'); }); -router.get('/', requireJwtAuth, async function (req, res) { +router.get('/', async function (req, res) { try { - let user = req.user.id; - user = user ?? null; + let user = req.user.id ?? ''; const { q } = req.query; const pageNumber = req.query.pageNumber || 1; - const key = `${user || ''}${q}`; - - if (cache.has(key)) { + const key = `${user}:search:${q}`; + const cached = await cache.get(key); + if (cached) { console.log('cache hit', key); - const cached = cache.get(key); const { pages, pageSize, messages } = cached; res .status(200) .send({ conversations: cached[pageNumber], pages, pageNumber, pageSize, messages }); return; - } else { - cache.clear(); } // const message = await Message.meiliSearch(q); @@ -67,7 +71,7 @@ router.get('/', requireJwtAuth, async function (req, res) { if (message.conversationId.includes('--')) { message.conversationId = cleanUpPrimaryKeyValue(message.conversationId); } - if (result.convoMap[message.conversationId] && !message.error) { + if (result.convoMap[message.conversationId]) { const convo = result.convoMap[message.conversationId]; const { title, chatGptLabel, model } = convo; message = { ...message, ...{ title, chatGptLabel, model } }; @@ -77,7 +81,7 @@ router.get('/', requireJwtAuth, async function (req, res) { result.messages = activeMessages; if (result.cache) { result.cache.messages = activeMessages; - cache.set(key, result.cache); + cache.set(key, result.cache, expiration); delete result.cache; } delete result.convoMap; diff --git a/api/server/services/ModelService.js b/api/server/services/ModelService.js index 41d4290ff..7789e70cf 100644 --- a/api/server/services/ModelService.js +++ b/api/server/services/ModelService.js @@ -1,9 +1,13 @@ const Keyv = require('keyv'); const axios = require('axios'); +const { isEnabled } = require('../utils'); +const keyvRedis = require('../../cache/keyvRedis'); // const { getAzureCredentials, genAzureChatCompletion } = require('../../utils/'); const { openAIApiKey, userProvidedOpenAI } = require('./EndpointService').config; -const modelsCache = new Keyv({ namespace: 'models' }); +const modelsCache = isEnabled(process.env.USE_REDIS) + ? new Keyv({ store: keyvRedis }) + : new Keyv({ namespace: 'models' }); const { OPENROUTER_API_KEY, OPENAI_REVERSE_PROXY, CHATGPT_MODELS, ANTHROPIC_MODELS } = process.env ?? {}; diff --git a/api/server/socialLogins.js b/api/server/socialLogins.js index ece87a2ed..af61db73e 100644 --- a/api/server/socialLogins.js +++ b/api/server/socialLogins.js @@ -1,4 +1,5 @@ const session = require('express-session'); +const RedisStore = require('connect-redis').default; const passport = require('passport'); const { googleLogin, @@ -7,6 +8,7 @@ const { facebookLogin, setupOpenId, } = require('../strategies'); +const client = require('../cache/redis'); const configureSocialLogins = (app) => { if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) { @@ -28,13 +30,15 @@ const configureSocialLogins = (app) => { process.env.OPENID_SCOPE && process.env.OPENID_SESSION_SECRET ) { - app.use( - session({ - secret: process.env.OPENID_SESSION_SECRET, - resave: false, - saveUninitialized: false, - }), - ); + const sessionOptions = { + secret: process.env.OPENID_SESSION_SECRET, + resave: false, + saveUninitialized: false, + }; + if (process.env.USE_REDIS) { + sessionOptions.store = new RedisStore({ client, prefix: 'librechat' }); + } + app.use(session(sessionOptions)); app.use(passport.session()); setupOpenId(); } diff --git a/bun.lockb b/bun.lockb index 1e71e31ce80da5d21de09df0b2b7b25015e9b9f6..f1262564fe3033bc87361f5cab5612ee907d5775 100755 GIT binary patch delta 51768 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 Hq4$3QY^Jkn delta 49193 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 { try { JSON.parse(str); } catch (e) { - console.error(e); return false; } return true; diff --git a/client/src/components/Messages/Message.tsx b/client/src/components/Messages/Message.tsx index ef857f98a..8f6f50e35 100644 --- a/client/src/components/Messages/Message.tsx +++ b/client/src/components/Messages/Message.tsx @@ -170,7 +170,7 @@ export default function Message({ text={text ?? ''} message={message} enterEdit={enterEdit} - error={error ?? false} + error={!!(error && !searchResult)} isSubmitting={isSubmitting} unfinished={unfinished ?? false} isCreatedByUser={isCreatedByUser ?? true} diff --git a/client/src/components/Nav/Nav.tsx b/client/src/components/Nav/Nav.tsx index 4bf54b583..fae97366f 100644 --- a/client/src/components/Nav/Nav.tsx +++ b/client/src/components/Nav/Nav.tsx @@ -13,7 +13,6 @@ import { Panel, Spinner } from '~/components'; import { Conversations, Pages } from '../Conversations'; import { useAuthContext, - useDebounce, useMediaQuery, useLocalize, useConversation, @@ -67,14 +66,8 @@ export default function Nav({ navVisible, setNavVisible }) { const [isFetching, setIsFetching] = useState(false); - const debouncedSearchTerm = useDebounce(searchQuery, 750); - const searchQueryFn = useSearchQuery(debouncedSearchTerm, pageNumber + '', { - enabled: !!( - !!debouncedSearchTerm && - debouncedSearchTerm.length > 0 && - isSearchEnabled && - isSearching - ), + const searchQueryFn = useSearchQuery(searchQuery, pageNumber + '', { + enabled: !!(!!searchQuery && searchQuery.length > 0 && isSearchEnabled && isSearching), }); const onSearchSuccess = useCallback((data: TSearchResults, expectedPage?: number) => { diff --git a/client/src/components/Nav/NavLinks.tsx b/client/src/components/Nav/NavLinks.tsx index e04b6ed27..8ca9febb0 100644 --- a/client/src/components/Nav/NavLinks.tsx +++ b/client/src/components/Nav/NavLinks.tsx @@ -55,6 +55,7 @@ export default function NavLinks() { 'group-ui-open:bg-gray-800 flex w-full items-center gap-2.5 rounded-md px-3 py-3 text-sm transition-colors duration-200 hover:bg-gray-800', open ? 'bg-gray-800' : '', )} + data-testid="nav-user" >
diff --git a/client/src/components/Nav/SearchBar.tsx b/client/src/components/Nav/SearchBar.tsx index 142daafa5..6f6fe6c21 100644 --- a/client/src/components/Nav/SearchBar.tsx +++ b/client/src/components/Nav/SearchBar.tsx @@ -1,6 +1,7 @@ -import { forwardRef, useState, useEffect, Ref } from 'react'; +import { forwardRef, useState, useCallback, useMemo, Ref } from 'react'; import { Search, X } from 'lucide-react'; -import { useRecoilState } from 'recoil'; +import { useSetRecoilState } from 'recoil'; +import debounce from 'lodash/debounce'; import { useLocalize } from '~/hooks'; import store from '~/store'; @@ -10,33 +11,35 @@ type SearchBarProps = { const SearchBar = forwardRef((props: SearchBarProps, ref: Ref) => { const { clearSearch } = props; - const [searchQuery, setSearchQuery] = useRecoilState(store.searchQuery); + const setSearchQuery = useSetRecoilState(store.searchQuery); const [showClearIcon, setShowClearIcon] = useState(false); + const [text, setText] = useState(''); const localize = useLocalize(); + const clearText = useCallback(() => { + setShowClearIcon(false); + setSearchQuery(''); + clearSearch(); + setText(''); + }, [setSearchQuery, clearSearch]); + const handleKeyUp = (e: React.KeyboardEvent) => { const { value } = e.target as HTMLInputElement; - /* TODO: deprecated keyCode */ - if (e.keyCode === 8 && value === '') { - setSearchQuery(''); - clearSearch(); + if (e.key === 'Backspace' && value === '') { + clearText(); } }; + const sendRequest = useCallback((value: string) => setSearchQuery(value), [setSearchQuery]); + const debouncedSendRequest = useMemo(() => debounce(sendRequest, 350), [sendRequest]); + const onChange = (e: React.FormEvent) => { const { value } = e.target as HTMLInputElement; - setSearchQuery(value); setShowClearIcon(value.length > 0); + setText(value); + debouncedSendRequest(value); }; - useEffect(() => { - if (searchQuery.length === 0) { - setShowClearIcon(false); - } else { - setShowClearIcon(true); - } - }, [searchQuery]); - return (
) = { e.code === 'Space' ? e.stopPropagation() : null; @@ -58,10 +61,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref) = className={`absolute right-3 h-5 w-5 cursor-pointer ${ showClearIcon ? 'opacity-100' : 'opacity-0' } transition-opacity duration-1000`} - onClick={() => { - setSearchQuery(''); - clearSearch(); - }} + onClick={clearText} />
); diff --git a/client/src/routes/Root.tsx b/client/src/routes/Root.tsx index 4da530b2d..c4711fb02 100644 --- a/client/src/routes/Root.tsx +++ b/client/src/routes/Root.tsx @@ -29,8 +29,8 @@ export default function Root() { const setEndpointsConfig = useSetRecoilState(store.endpointsConfig); const setModelsConfig = useSetRecoilState(store.modelsConfig); - const searchEnabledQuery = useGetSearchEnabledQuery(); const endpointsQuery = useGetEndpointsQuery(); + const searchEnabledQuery = useGetSearchEnabledQuery({ enabled: isAuthenticated }); const modelsQuery = useGetModelsQuery({ enabled: isAuthenticated }); const presetsQuery = useGetPresetsQuery({ enabled: !!user }); diff --git a/client/src/store/endpoints.ts b/client/src/store/endpoints.ts index cbcd3e54f..458c40ad3 100644 --- a/client/src/store/endpoints.ts +++ b/client/src/store/endpoints.ts @@ -1,17 +1,19 @@ import { atom, selector } from 'recoil'; import { TEndpointsConfig } from 'librechat-data-provider'; +const defaultConfig: TEndpointsConfig = { + azureOpenAI: null, + openAI: null, + bingAI: null, + chatGPTBrowser: null, + gptPlugins: null, + google: null, + anthropic: null, +}; + const endpointsConfig = atom({ key: 'endpointsConfig', - default: { - azureOpenAI: null, - openAI: null, - bingAI: null, - chatGPTBrowser: null, - gptPlugins: null, - google: null, - anthropic: null, - }, + default: defaultConfig, }); const plugins = selector({ @@ -58,4 +60,5 @@ export default { endpointsConfig, endpointsFilter, availableEndpoints, + defaultConfig, }; diff --git a/client/src/utils/getDefaultEndpoint.ts b/client/src/utils/getDefaultEndpoint.ts index 1b960996e..4f1e9f74b 100644 --- a/client/src/utils/getDefaultEndpoint.ts +++ b/client/src/utils/getDefaultEndpoint.ts @@ -28,11 +28,18 @@ const getEndpointFromSetup = (convoSetup: TConvoSetup, endpointsConfig: TEndpoin const getEndpointFromLocalStorage = (endpointsConfig: TEndpointsConfig) => { try { const { lastConversationSetup } = getLocalStorageItems(); + const { endpoint } = lastConversationSetup; + const isDefaultConfig = Object.values(endpointsConfig ?? {})?.every((value) => !value); - return ( - lastConversationSetup.endpoint && - (endpointsConfig[lastConversationSetup.endpoint] ? lastConversationSetup.endpoint : null) - ); + if (isDefaultConfig && endpoint) { + return endpoint; + } + + if (isDefaultConfig && endpoint) { + return endpoint; + } + + return endpoint && endpointsConfig[endpoint] ? endpoint : null; } catch (error) { console.error(error); return null; diff --git a/e2e/setup/authenticate.ts b/e2e/setup/authenticate.ts index 9e91efd9f..6abac7a7e 100644 --- a/e2e/setup/authenticate.ts +++ b/e2e/setup/authenticate.ts @@ -1,8 +1,10 @@ import { Page, FullConfig, chromium } from '@playwright/test'; +import cleanupUser from './cleanupUser'; import dotenv from 'dotenv'; dotenv.config(); type User = { email: string; name: string; password: string }; +const timeout = 3500; async function register(page: Page, user: User) { await page.getByRole('link', { name: 'Sign up' }).click(); @@ -52,18 +54,31 @@ async function authenticate(config: FullConfig, user: User) { }); console.log('🤖: ✔️ localStorage: set Nav as Visible', storageState); - await page.goto(baseURL, { timeout: 5000 }); + await page.goto(baseURL, { timeout }); await register(page, user); - await page.waitForURL(`${baseURL}/chat/new`); + try { + await page.waitForURL(`${baseURL}/chat/new`, { timeout }); + } catch (error) { + console.error('Error:', error); + const userExists = page.getByTestId('registration-error'); + if (userExists) { + console.log('🤖: 🚨 user already exists'); + await cleanupUser(user); + await page.goto(baseURL, { timeout }); + await register(page, user); + } else { + throw new Error('🤖: 🚨 user failed to register'); + } + } console.log('🤖: ✔️ user successfully registered'); // Logout await logout(page, user); - await page.waitForURL(`${baseURL}/login`); + await page.waitForURL(`${baseURL}/login`, { timeout }); console.log('🤖: ✔️ user successfully logged out'); await login(page, user); - await page.waitForURL(`${baseURL}/chat/new`); + await page.waitForURL(`${baseURL}/chat/new`, { timeout }); console.log('🤖: ✔️ user successfully authenticated'); await page.context().storageState({ path: storageState as string }); diff --git a/e2e/specs/messages.spec.ts b/e2e/specs/messages.spec.ts index 76cf0a540..f9593c06c 100644 --- a/e2e/specs/messages.spec.ts +++ b/e2e/specs/messages.spec.ts @@ -38,14 +38,6 @@ test.beforeAll(async ({ browser }) => { await page.close(); }); -test.afterAll(async () => { - console.log('🤖: clearing conversations after message tests.'); - const page = await beforeAfterAllContext.newPage(); - await clearConvos(page); - await page.close(); - await beforeAfterAllContext.close(); -}); - test.beforeEach(async ({ page }) => { await page.goto(initialUrl, { timeout: 5000 }); }); diff --git a/e2e/specs/nav.spec.ts b/e2e/specs/nav.spec.ts index d8f997058..ff1f5e16b 100644 --- a/e2e/specs/nav.spec.ts +++ b/e2e/specs/nav.spec.ts @@ -4,14 +4,14 @@ test.describe('Navigation suite', () => { test('Navigation bar', async ({ page }) => { await page.goto('http://localhost:3080/', { timeout: 5000 }); - await page.locator('[id="headlessui-menu-button-\\:r0\\:"]').click(); - const navBar = await page.locator('[id="headlessui-menu-button-\\:r0\\:"]').isVisible(); - expect(navBar).toBeTruthy(); + await page.getByTestId('nav-user').click(); + const navSettings = await page.getByTestId('nav-user').isVisible(); + expect(navSettings).toBeTruthy(); }); test('Settings modal', async ({ page }) => { await page.goto('http://localhost:3080/', { timeout: 5000 }); - await page.locator('[id="headlessui-menu-button-\\:r0\\:"]').click(); + await page.getByTestId('nav-user').click(); await page.getByText('Settings').click(); const modal = await page.getByRole('dialog', { name: 'Settings' }).isVisible(); diff --git a/package-lock.json b/package-lock.json index c4ca442c9..a676c063e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "packages/*" ], "devDependencies": { - "@playwright/test": "^1.32.1", + "@playwright/test": "^1.38.1", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "cross-env": "^7.0.3", @@ -45,11 +45,13 @@ "@anthropic-ai/sdk": "^0.5.4", "@azure/search-documents": "^11.3.2", "@keyv/mongo": "^2.1.8", + "@keyv/redis": "^2.8.0", "@waylaidwanderer/chatgpt-api": "^1.37.2", "axios": "^1.3.4", "bcryptjs": "^2.4.3", "cheerio": "^1.0.0-rc.12", "cohere-ai": "^6.0.0", + "connect-redis": "^7.1.0", "cookie": "^0.5.0", "cors": "^2.8.5", "dotenv": "^16.0.3", @@ -60,10 +62,11 @@ "googleapis": "^118.0.0", "handlebars": "^4.7.7", "html": "^1.0.0", + "ioredis": "^5.3.2", "jose": "^4.15.2", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.0", - "keyv": "^4.5.3", + "keyv": "^4.5.4", "keyv-file": "^0.2.0", "langchain": "^0.0.153", "lodash": "^4.17.21", @@ -4850,6 +4853,11 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -5419,6 +5427,17 @@ "@mongodb-js/saslprep": "^1.1.0" } }, + "node_modules/@keyv/redis": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@keyv/redis/-/redis-2.8.0.tgz", + "integrity": "sha512-6k7wG/KKSIGpruKlsEB4sFjECJEyQsuJbWoWdoq9Uv2L6Mm/SEqEidekRZI/QljE1A4WQkFsIE8hHl1Oc3UNGg==", + "dependencies": { + "ioredis": "^5.3.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/@librechat/backend": { "resolved": "api", "link": true @@ -5531,12 +5550,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.0.tgz", - "integrity": "sha512-xis/RXXsLxwThKnlIXouxmIvvT3zvQj1JE39GsNieMUrMpb3/GySHDh2j8itCG22qKVD4MYLBp7xB73cUW/UUw==", + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz", + "integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==", "dev": true, "dependencies": { - "playwright": "1.38.0" + "playwright": "1.38.1" }, "bin": { "playwright": "cli.js" @@ -6500,6 +6519,78 @@ "node": ">=8.x" } }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "optional": true, + "peer": true, + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.5.11", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.11.tgz", + "integrity": "sha512-cV7yHcOAtNQ5x/yQl7Yw1xf53kO0FNDTdDU6bFIMbW6ljB7U7ns0YRM+QIkpoqTAt6zK5k9Fq0QWlUbLcq9AvA==", + "optional": true, + "peer": true, + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/client/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true, + "peer": true + }, + "node_modules/@redis/graph": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz", + "integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==", + "optional": true, + "peer": true, + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.6.tgz", + "integrity": "sha512-rcZO3bfQbm2zPRpqo82XbW8zg4G/w4W3tI7X8Mqleq9goQjAGLL7q/1n1ZX4dXEAmORVZ4s1+uKLaUOg7LrUhw==", + "optional": true, + "peer": true, + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.5.tgz", + "integrity": "sha512-hPP8w7GfGsbtYEJdn4n7nXa6xt6hVZnnDktKW4ArMaFQ/m/aR7eFvsLQmG/mn1Upq99btPJk+F27IQ2dYpCoUg==", + "optional": true, + "peer": true, + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.5.tgz", + "integrity": "sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg==", + "optional": true, + "peer": true, + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, "node_modules/@remix-run/router": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.9.0.tgz", @@ -9682,6 +9773,14 @@ "node": ">=6" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -9856,6 +9955,17 @@ "node": ">=0.8" } }, + "node_modules/connect-redis": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-7.1.0.tgz", + "integrity": "sha512-UaqO1EirWjON2ENsyau7N5lbkrdYBpS6mYlXSeff/OYXsd6EGZ+SXSmNPoljL2PSua8fgjAEaldSA73PMZQ9Eg==", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "express-session": ">=1" + } + }, "node_modules/consola": { "version": "2.15.3", "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", @@ -10399,6 +10509,14 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -12368,6 +12486,16 @@ "node": ">=12" } }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -13636,6 +13764,29 @@ "loose-envify": "^1.0.0" } }, + "node_modules/ioredis": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz", + "integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ip": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", @@ -15222,9 +15373,9 @@ } }, "node_modules/keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dependencies": { "json-buffer": "3.0.1" } @@ -15583,11 +15734,21 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -18422,12 +18583,12 @@ } }, "node_modules/playwright": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.0.tgz", - "integrity": "sha512-fJGw+HO0YY+fU/F1N57DMO+TmXHTrmr905J05zwAQE9xkuwP/QLDk63rVhmyxh03dYnEhnRbsdbH9B0UVVRB3A==", + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz", + "integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==", "devOptional": true, "dependencies": { - "playwright-core": "1.38.0" + "playwright-core": "1.38.1" }, "bin": { "playwright": "cli.js" @@ -18440,9 +18601,9 @@ } }, "node_modules/playwright-core": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.0.tgz", - "integrity": "sha512-f8z1y8J9zvmHoEhKgspmCvOExF2XdcxMW8jNRuX4vkQFrzV4MlZ55iwb5QeyiFQgOFCUolXiRHgpjSEnqvO48g==", + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz", + "integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==", "devOptional": true, "bin": { "playwright-core": "cli.js" @@ -20348,6 +20509,40 @@ "node": ">=8" } }, + "node_modules/redis": { + "version": "4.6.10", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.10.tgz", + "integrity": "sha512-mmbyhuKgDiJ5TWUhiKhBssz+mjsuSI/lSZNPI9QvZOYzWvYGejtb+W3RlDDf8LD6Bdl5/mZeG8O1feUGhXTxEg==", + "optional": true, + "peer": true, + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.5.11", + "@redis/graph": "1.1.0", + "@redis/json": "1.0.6", + "@redis/search": "1.1.5", + "@redis/time-series": "1.0.5" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", @@ -21397,6 +21592,11 @@ "node": ">=8" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", diff --git a/package.json b/package.json index 3a9578c8e..eb6401d37 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ }, "homepage": "https://github.com/danny-avila/LibreChat#readme", "devDependencies": { - "@playwright/test": "^1.32.1", + "@playwright/test": "^1.38.1", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "cross-env": "^7.0.3",