diff --git a/.env.example b/.env.example index da97e42e68..3baabd3fa9 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,8 @@ # Server configuration: ########################## +APP_TITLE=LibreChat + # The server will listen to localhost:3080 by default. You can change the target IP as you want. # If you want to make this server available externally, for example to share the server with others # or expose this from a Docker container, set host to 0.0.0.0 or your external IP interface. @@ -171,6 +173,9 @@ MEILI_MASTER_KEY=DrhYf7zENyR6AlUCKmnz0eYASOQdl6zxH7s7MKFSfFCt # User System: ########################## +# Allow Public Registration +ALLOW_REGISTRATION=true + # JWT Secrets JWT_SECRET=secret JWT_REFRESH_SECRET=secret @@ -197,19 +202,3 @@ SESSION_EXPIRY=(1000 * 60 * 60 * 24) * 7 DOMAIN_CLIENT=http://localhost:3080 DOMAIN_SERVER=http://localhost:3080 - -########################### -# Frontend Configuration (Vite): -########################### - -# Custom app name, this text will be displayed in the landing page and the footer. -VITE_APP_TITLE="LibreChat" - -# Enable Social Login -# This enables/disables the Login with Google button on the login page. -# Set to true if you have registered the app with google cloud services -# and have set the GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET above -VITE_SHOW_GOOGLE_LOGIN_OPTION=false - -# Allow Public Registration -ALLOW_REGISTRATION=true diff --git a/.github/workflows/backend-review.yml b/.github/workflows/backend-review.yml index fb6754b9dc..07a2f85d6c 100644 --- a/.github/workflows/backend-review.yml +++ b/.github/workflows/backend-review.yml @@ -1,4 +1,3 @@ - name: Backend Unit Tests on: push: @@ -34,7 +33,7 @@ jobs: run: npm ci # - name: Install Linux X64 Sharp - # run: npm install --platform=linux --arch=x64 --verbose sharp + # run: npm install --platform=linux --arch=x64 --verbose sharp - name: Run unit tests - run: cd api && npm run test:ci \ No newline at end of file + run: cd api && npm run test:ci diff --git a/api/package.json b/api/package.json index a032aac3d6..873a5a62ff 100644 --- a/api/package.json +++ b/api/package.json @@ -64,6 +64,7 @@ "devDependencies": { "jest": "^29.5.0", "nodemon": "^2.0.20", - "path": "^0.12.7" + "path": "^0.12.7", + "supertest": "^6.3.3" } } diff --git a/api/server/index.js b/api/server/index.js index 3e95b2425e..5a4104ab28 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -54,6 +54,7 @@ config.validate(); // Validate the config app.use('/api/tokenizer', routes.tokenizer); app.use('/api/endpoints', routes.endpoints); app.use('/api/plugins', routes.plugins); + app.use('/api/config', routes.config); // static files app.get('/*', function (req, res) { diff --git a/api/server/routes/__tests__/config.spec.js b/api/server/routes/__tests__/config.spec.js new file mode 100644 index 0000000000..88e79840f0 --- /dev/null +++ b/api/server/routes/__tests__/config.spec.js @@ -0,0 +1,37 @@ +const request = require('supertest'); +const express = require('express'); +const routes = require('../'); + +const app = express(); +app.use('/api/config', routes.config); + +afterEach(() => { + delete process.env.APP_TITLE; + delete process.env.GOOGLE_CLIENT_ID; + delete process.env.GOOGLE_CLIENT_SECRET; + delete process.env.DOMAIN_SERVER; + delete process.env.ALLOW_REGISTRATION; +}); + +//TODO: This works/passes locally but http request tests fail with 404 in CI. Need to figure out why. + +// eslint-disable-next-line jest/no-disabled-tests +describe.skip('GET /', () => { + it('should return 200 and the correct body', async () => { + process.env.APP_TITLE = 'Test Title'; + process.env.GOOGLE_CLIENT_ID = 'Test Google Client Id'; + process.env.GOOGLE_CLIENT_SECRET = 'Test Google Client Secret'; + process.env.DOMAIN_SERVER = 'http://test-server.com'; + process.env.ALLOW_REGISTRATION = 'true'; + + const response = await request(app).get('/'); + + expect(response.statusCode).toBe(200); + expect(response.body).toEqual({ + appTitle: 'Test Title', + googleLoginEnabled: true, + serverDomain: 'http://test-server.com', + registrationEnabled: 'true', + }); + }); +}); \ No newline at end of file diff --git a/api/server/routes/config.js b/api/server/routes/config.js new file mode 100644 index 0000000000..f91f3c78d4 --- /dev/null +++ b/api/server/routes/config.js @@ -0,0 +1,18 @@ +const express = require('express'); +const router = express.Router(); + +router.get('/', async function (req, res) { + try { + const appTitle = process.env.APP_TITLE || 'LibreChat'; + const googleLoginEnabled = !!process.env.GOOGLE_CLIENT_ID && !!process.env.GOOGLE_CLIENT_SECRET; + const serverDomain = process.env.DOMAIN_SERVER || 'http://localhost:3080'; + const registrationEnabled = process.env.ALLOW_REGISTRATION || true; + + return res.status(200).send({appTitle, googleLoginEnabled, serverDomain, registrationEnabled}); + } catch (err) { + console.error(err); + return res.status(500).send({error: err.message}); + } +}); + +module.exports = router; diff --git a/api/server/routes/index.js b/api/server/routes/index.js index 6df9ae3d23..f41f5c3baa 100644 --- a/api/server/routes/index.js +++ b/api/server/routes/index.js @@ -10,6 +10,7 @@ const oauth = require('./oauth'); const { router: endpoints } = require('./endpoints'); const plugins = require('./plugins'); const user = require('./user'); +const config = require('./config'); module.exports = { search, @@ -23,5 +24,6 @@ module.exports = { user, tokenizer, endpoints, - plugins + plugins, + config }; diff --git a/client/src/components/Auth/Login.tsx b/client/src/components/Auth/Login.tsx index 1238970d79..a7d8fcf338 100644 --- a/client/src/components/Auth/Login.tsx +++ b/client/src/components/Auth/Login.tsx @@ -2,10 +2,11 @@ import { useEffect } from 'react'; import LoginForm from './LoginForm'; import { useAuthContext } from '~/hooks/AuthContext'; import { useNavigate } from 'react-router-dom'; -import { SHOW_GOOGLE_LOGIN_OPTION, ALLOW_REGISTRATION, DOMAIN_SERVER } from "~/utils/envConstants"; +import { useGetStartupConfig } from '~/data-provider'; function Login() { const { login, error, isAuthenticated } = useAuthContext(); + const { data: startupConfig } = useGetStartupConfig(); const navigate = useNavigate(); @@ -29,7 +30,7 @@ function Login() { )} - {ALLOW_REGISTRATION && ( + {startupConfig?.registrationEnabled && (

{' '} Don't have an account?{' '} @@ -38,7 +39,7 @@ function Login() {

)} - {SHOW_GOOGLE_LOGIN_OPTION && ( + {startupConfig?.googleLoginEnabled && ( <>
Or
@@ -47,7 +48,7 @@ function Login() { { + if (startupConfig?.registrationEnabled === false) { + navigate('/login'); + } + }, [startupConfig, navigate]); + return (
@@ -266,7 +272,7 @@ function Registration() { Login

- {SHOW_GOOGLE_LOGIN_OPTION && ( + {startupConfig?.googleLoginEnabled && ( <>
Or
@@ -275,7 +281,7 @@ function Registration() {
({ - DOMAIN_SERVER: 'mock-server', - SHOW_GOOGLE_LOGIN_OPTION: true, - ALLOW_REGISTRATION: true -})); - jest.mock('~/data-provider'); const setup = ({ @@ -23,6 +17,15 @@ const setup = ({ mutate: jest.fn(), data: {}, isSuccess: false + }, + useGetStartupCongfigReturnValue = { + isLoading: false, + isError: false, + data: { + googleLoginEnabled: true, + registrationEnabled: true, + serverDomain: 'mock-server' + } } } = {}) => { const mockUseLoginUser = jest @@ -33,12 +36,16 @@ const setup = ({ .spyOn(mockDataProvider, 'useGetUserQuery') //@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult .mockReturnValue(useGetUserQueryReturnValue); + const mockUseGetStartupConfig = jest + .spyOn(mockDataProvider, 'useGetStartupConfig') + //@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult + .mockReturnValue(useGetStartupCongfigReturnValue); const renderResult = render(); - return { ...renderResult, mockUseLoginUser, - mockUseGetUserQuery + mockUseGetUserQuery, + mockUseGetStartupConfig }; }; diff --git a/client/src/components/Auth/__tests__/Registration.spec.tsx b/client/src/components/Auth/__tests__/Registration.spec.tsx index 68e0082fdf..f7c9498d0c 100644 --- a/client/src/components/Auth/__tests__/Registration.spec.tsx +++ b/client/src/components/Auth/__tests__/Registration.spec.tsx @@ -3,11 +3,6 @@ import userEvent from '@testing-library/user-event'; import Registration from '../Registration'; import * as mockDataProvider from '~/data-provider'; -jest.mock('~/utils/envConstants', () => ({ - DOMAIN_SERVER: 'mock-server', - SHOW_GOOGLE_LOGIN_OPTION: true -})); - jest.mock('~/data-provider'); const setup = ({ @@ -22,6 +17,15 @@ const setup = ({ mutate: jest.fn(), data: {}, isSuccess: false + }, + useGetStartupCongfigReturnValue = { + isLoading: false, + isError: false, + data: { + googleLoginEnabled: true, + registrationEnabled: true, + serverDomain: 'mock-server' + } } } = {}) => { const mockUseRegisterUserMutation = jest @@ -32,13 +36,18 @@ const setup = ({ .spyOn(mockDataProvider, 'useGetUserQuery') //@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult .mockReturnValue(useGetUserQueryReturnValue); + const mockUseGetStartupConfig = jest + .spyOn(mockDataProvider, 'useGetStartupConfig') + //@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult + .mockReturnValue(useGetStartupCongfigReturnValue); const renderResult = render(); return { ...renderResult, mockUseRegisterUserMutation, - mockUseGetUserQuery + mockUseGetUserQuery, + mockUseGetStartupConfig }; }; diff --git a/client/src/components/Input/Footer.jsx b/client/src/components/Input/Footer.tsx similarity index 78% rename from client/src/components/Input/Footer.jsx rename to client/src/components/Input/Footer.tsx index d1c75ab309..28c61736a2 100644 --- a/client/src/components/Input/Footer.jsx +++ b/client/src/components/Input/Footer.tsx @@ -1,6 +1,8 @@ import React from 'react'; +import { useGetStartupConfig } from '~/data-provider'; export default function Footer() { + const { data: config } = useGetStartupConfig(); return (
- {import.meta.env.VITE_APP_TITLE || 'LibreChat'} + {config?.appTitle || 'LibreChat'} . Serves and searches all conversations reliably. All AI convos under one house. Pay per call and not per month (cents compared to dollars). diff --git a/client/src/components/ui/Landing.jsx b/client/src/components/ui/Landing.tsx similarity index 92% rename from client/src/components/ui/Landing.jsx rename to client/src/components/ui/Landing.tsx index 4cd1f090fc..26745b5c45 100644 --- a/client/src/components/ui/Landing.jsx +++ b/client/src/components/ui/Landing.tsx @@ -1,20 +1,24 @@ +import React from 'react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import useDocumentTitle from '~/hooks/useDocumentTitle'; import SunIcon from '../svg/SunIcon'; import LightningIcon from '../svg/LightningIcon'; import CautionIcon from '../svg/CautionIcon'; import store from '~/store'; +import { useGetStartupConfig } from '~/data-provider'; export default function Landing() { + const { data: config } = useGetStartupConfig(); const setText = useSetRecoilState(store.text); const conversation = useRecoilValue(store.conversation); + // @ts-ignore TODO: Fix anti-pattern - requires refactoring conversation store const { title = 'New Chat' } = conversation || {}; useDocumentTitle(title); - const clickHandler = (e) => { + const clickHandler = (e: React.MouseEvent) => { e.preventDefault(); - const { innerText } = e.target; + const { innerText } = e.target as HTMLButtonElement; const quote = innerText.split('"')[1].trim(); setText(quote); }; @@ -26,7 +30,7 @@ export default function Landing() { id="landing-title" className="mb-10 ml-auto mr-auto mt-6 flex items-center justify-center gap-2 text-center text-4xl font-semibold sm:mb-16 md:mt-[10vh]" > - {import.meta.env.VITE_APP_TITLE || 'LibreChat'} + {config?.appTitle || 'LibreChat'}
diff --git a/client/src/data-provider/api-endpoints.ts b/client/src/data-provider/api-endpoints.ts index dc0a285a2f..3fdbd43eb7 100644 --- a/client/src/data-provider/api-endpoints.ts +++ b/client/src/data-provider/api-endpoints.ts @@ -89,3 +89,7 @@ export const resetPassword = () => { export const plugins = () => { return '/api/plugins'; }; + +export const config = () => { + return '/api/config'; +} diff --git a/client/src/data-provider/data-service.ts b/client/src/data-provider/data-service.ts index 0d5247d840..e89e215e87 100644 --- a/client/src/data-provider/data-service.ts +++ b/client/src/data-provider/data-service.ts @@ -111,3 +111,7 @@ export const getAvailablePlugins = (): Promise => { export const updateUserPlugins = (payload: t.TUpdateUserPlugins) => { return request.post(endpoints.userPlugins(), payload); }; + +export const getStartupConfig = (): Promise => { + return request.get(endpoints.config()); +} diff --git a/client/src/data-provider/react-query-service.ts b/client/src/data-provider/react-query-service.ts index fe8ca1ff7f..128f02f616 100644 --- a/client/src/data-provider/react-query-service.ts +++ b/client/src/data-provider/react-query-service.ts @@ -19,7 +19,8 @@ export enum QueryKeys { presets = 'presets', searchResults = 'searchResults', tokenCount = 'tokenCount', - availablePlugins = 'availablePlugins' + availablePlugins = 'availablePlugins', + startupConfig = 'startupConfig', } export const useAbortRequestWithMessage = (): UseMutationResult< @@ -336,3 +337,11 @@ export const useUpdateUserPluginsMutation = (): UseMutationResult< } }); }; + +export const useGetStartupConfig = (): QueryObserverResult => { + return useQuery([QueryKeys.startupConfig], () => dataService.getStartupConfig(), { + refetchOnWindowFocus: false, + refetchOnReconnect: false, + refetchOnMount: false + }); +} diff --git a/client/src/data-provider/types.ts b/client/src/data-provider/types.ts index c7ec6a92a0..3d4433608a 100644 --- a/client/src/data-provider/types.ts +++ b/client/src/data-provider/types.ts @@ -233,3 +233,10 @@ export type TResetPassword = { token: string; password: string; }; + +export type TStartupConfig = { + appTitle: boolean; + googleLoginEnabled: boolean; + serverDomain: string; + registrationEnabled: boolean; +} diff --git a/client/src/routes/Chat.jsx b/client/src/routes/Chat.jsx index a65a2ef681..d7de6e73c2 100644 --- a/client/src/routes/Chat.jsx +++ b/client/src/routes/Chat.jsx @@ -7,7 +7,11 @@ import Messages from '../components/Messages'; import TextChat from '../components/Input'; import store from '~/store'; -import { useGetMessagesByConvoId, useGetConversationByIdMutation } from '~/data-provider'; +import { + useGetMessagesByConvoId, + useGetConversationByIdMutation, + useGetStartupConfig +} from '~/data-provider'; export default function Chat() { const searchQuery = useRecoilValue(store.searchQuery); @@ -21,6 +25,7 @@ export default function Chat() { //disabled by default, we only enable it when messagesTree is null const messagesQuery = useGetMessagesByConvoId(conversationId, { enabled: false }); const getConversationMutation = useGetConversationByIdMutation(conversationId); + const { data: config } = useGetStartupConfig(); // when conversation changed or conversationId (in url) changed useEffect(() => { @@ -53,8 +58,8 @@ export default function Chat() { // conversationId (in url) should always follow conversation?.conversationId, unless conversation is null navigate(`/chat/${conversation?.conversationId}`); } - document.title = conversation?.title || import.meta.env.VITE_APP_TITLE || 'Chat'; - }, [conversation, conversationId]); + document.title = conversation?.title || config?.appTitle || 'Chat'; + }, [conversation, conversationId, config]); useEffect(() => { if (messagesTree === null && conversation?.conversationId) { diff --git a/client/src/routes/index.jsx b/client/src/routes/index.jsx index 6454f7984c..ffd04cc6b1 100644 --- a/client/src/routes/index.jsx +++ b/client/src/routes/index.jsx @@ -5,7 +5,6 @@ import Search from './Search'; import { Login, Registration, RequestPasswordReset, ResetPassword } from '../components/Auth'; import { AuthContextProvider } from '../hooks/AuthContext'; import ApiErrorWatcher from '../components/Auth/ApiErrorWatcher'; -import { ALLOW_REGISTRATION } from '../utils/envConstants'; const AuthLayout = () => ( @@ -17,7 +16,7 @@ const AuthLayout = () => ( export const router = createBrowserRouter([ { path: 'register', - element: ALLOW_REGISTRATION ? : + element: }, { path: 'forgot-password', diff --git a/client/src/utils/envConstants.js b/client/src/utils/envConstants.js deleted file mode 100644 index 71135b550f..0000000000 --- a/client/src/utils/envConstants.js +++ /dev/null @@ -1,9 +0,0 @@ -const ALLOW_REGISTRATION = import.meta.env.ALLOW_REGISTRATION === 'true'; -const DOMAIN_SERVER = import.meta.env.DOMAIN_SERVER; -const SHOW_GOOGLE_LOGIN_OPTION = import.meta.env.VITE_SHOW_GOOGLE_LOGIN_OPTION === 'true'; - -export { - ALLOW_REGISTRATION, - DOMAIN_SERVER, - SHOW_GOOGLE_LOGIN_OPTION -}; diff --git a/config/install.js b/config/install.js index 134280156d..ba7baa5882 100644 --- a/config/install.js +++ b/config/install.js @@ -60,7 +60,7 @@ let env = {}; const title = await askQuestion( 'Enter the app title (default: "LibreChat"): ' ); - env['VITE_APP_TITLE'] = title || 'LibreChat'; + env['APP_TITLE'] = title || 'LibreChat'; // Ask for OPENAI_API_KEY const key = await askQuestion( diff --git a/config/upgrade.js b/config/upgrade.js index 9cac577802..8a2f6dd06b 100644 --- a/config/upgrade.js +++ b/config/upgrade.js @@ -95,6 +95,7 @@ const removeEnvs = { 'SERVER_URL_PROD': 'remove', 'JWT_SECRET_DEV': 'remove', // Lets regen 'JWT_SECRET_PROD': 'remove', // Lets regen + 'VITE_APP_TITLE': 'remove', // Comments to remove: '#JWT:': 'remove', '# Add a secure secret for production if deploying to live domain.': 'remove', @@ -120,11 +121,10 @@ loader.addSecureEnvVar(rootEnvPath, 'JWT_SECRET', 32); // Lets update the openai key name, not the best spot in the env file but who cares ¯\_(ツ)_/¯ loader.writeEnvFile(rootEnvPath, {'OPENAI_API_KEY': initEnv['OPENAI_KEY']}) -// TODO: we need to copy over the value of: VITE_SHOW_GOOGLE_LOGIN_OPTION & VITE_APP_TITLE +// TODO: we need to copy over the value of: APP_TITLE fs.appendFileSync(rootEnvPath, '\n\n##########################\n# Frontend Vite Variables:\n##########################\n'); const frontend = { - 'VITE_APP_TITLE': initEnv['VITE_APP_TITLE'] || '"LibreChat"', - 'VITE_SHOW_GOOGLE_LOGIN_OPTION': initEnv['VITE_SHOW_GOOGLE_LOGIN_OPTION'] || 'false', + 'APP_TITLE': initEnv['VITE_APP_TITLE'] || '"LibreChat"', 'ALLOW_REGISTRATION': 'true' } loader.writeEnvFile(rootEnvPath, frontend) diff --git a/docs/features/user_auth_system.md b/docs/features/user_auth_system.md index 056a551a24..fbde30dd1a 100644 --- a/docs/features/user_auth_system.md +++ b/docs/features/user_auth_system.md @@ -29,7 +29,7 @@ When the first account is registered, the application will automatically migrate The application is setup to support OAuth2/Social Login with Google. All of the code is in place for Facebook login as well, but this has not been tested because the setup process with Facebook was honestly just too painful for me to deal with. I plan to add support for other OAuth2 providers including Github and Discord at a later time. -To enable Google login, you must create an application in the [Google Cloud Console](https://cloud.google.com) and provide the client ID and client secret in the `/.env` file, then set `VITE_SHOW_GOOGLE_LOGIN_OPTION=true`. +To enable Google login, you must create an application in the [Google Cloud Console](https://cloud.google.com) and provide the client ID and client secret in the `/.env` file. ### *Instructions for setting up Google login are provided below.* ``` diff --git a/docs/install/docker_install.md b/docs/install/docker_install.md index 194205a765..bc46199e42 100644 --- a/docs/install/docker_install.md +++ b/docs/install/docker_install.md @@ -50,11 +50,10 @@ To update LibreChat. enter these commands one after the other from the root dir: - MEILI_HOST=http://meilisearch:7700 - MEILI_HTTP_ADDR=meilisearch:7700 ``` -- If you'd like to change the app title or disable/enable google login, edit the following lines (the ones in your .env file are not read during building) +- If you'd like to change the app title, edit the following lines (the ones in your .env file are not read during building) ```yaml args: - VITE_APP_TITLE: LibreChat # default, change to your desired app name - VITE_SHOW_GOOGLE_LOGIN_OPTION: false # default, change to true if you have google auth setup + APP_TITLE: LibreChat # default, change to your desired app name ``` - If for some reason you're not able to build the app image, you can pull the latest image from **Dockerhub**. @@ -67,8 +66,7 @@ To update LibreChat. enter these commands one after the other from the root dir: context: . target: node args: - VITE_APP_TITLE: LibreChat # default, change to your desired app name - VITE_SHOW_GOOGLE_LOGIN_OPTION: false # default, change to true if you have google auth setup + APP_TITLE: LibreChat # default, change to your desired app name ``` - Comment this line in (remove the `#` key) diff --git a/package-lock.json b/package-lock.json index 266debfca7..2447137425 100644 --- a/package-lock.json +++ b/package-lock.json @@ -81,7 +81,8 @@ "devDependencies": { "jest": "^29.5.0", "nodemon": "^2.0.20", - "path": "^0.12.7" + "path": "^0.12.7", + "supertest": "^6.3.3" } }, "api/node_modules/ansi-styles": { @@ -8340,6 +8341,12 @@ "node": ">=8" } }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, "node_modules/asn1.js": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", @@ -9883,6 +9890,12 @@ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "dev": true }, + "node_modules/component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -10013,6 +10026,12 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true + }, "node_modules/copy-anything": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", @@ -10638,6 +10657,16 @@ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -12087,6 +12116,12 @@ "node": ">=6" } }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, "node_modules/fast-text-encoding": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", @@ -12486,6 +12521,21 @@ "node": ">=0.4.x" } }, + "node_modules/formidable": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", + "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==", + "dev": true, + "dependencies": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -13273,6 +13323,15 @@ "he": "bin/he" } }, + "node_modules/hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/highlight.js": { "version": "11.8.0", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.8.0.tgz", @@ -23640,6 +23699,72 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/superagent": { + "version": "8.0.9", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.9.tgz", + "integrity": "sha512-4C7Bh5pyHTvU33KpZgwrNKh/VQnvgtCSqPRfJAUdmrtSYePVzVg4E4OzsrbkhJj9O7SO6Bnv75K/F8XVZT8YHA==", + "dev": true, + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.2", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=6.4.0 <13 || >=14" + } + }, + "node_modules/superagent/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/superagent/node_modules/semver": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/superagent/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/superjson": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.12.3.tgz", @@ -23652,6 +23777,19 @@ "node": ">=10" } }, + "node_modules/supertest": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.3.tgz", + "integrity": "sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA==", + "dev": true, + "dependencies": { + "methods": "^1.1.2", + "superagent": "^8.0.5" + }, + "engines": { + "node": ">=6.4.0" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -29339,7 +29477,8 @@ "path": "^0.12.7", "pino": "^8.12.1", "sanitize": "^2.1.2", - "sharp": "^0.32.1" + "sharp": "^0.32.1", + "supertest": "^6.3.3" }, "dependencies": { "ansi-styles": { @@ -31371,6 +31510,12 @@ "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, "asn1.js": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", @@ -32534,6 +32679,12 @@ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "dev": true }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -32647,6 +32798,12 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true + }, "copy-anything": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", @@ -33104,6 +33261,16 @@ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" }, + "dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "requires": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -34216,6 +34383,12 @@ "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.2.0.tgz", "integrity": "sha512-zaTadChr+NekyzallAMXATXLOR8MNx3zqpZ0MUF2aGf4EathnG0f32VLODNlY8IuGY3HoRO2L6/6fSzNsLaHIw==" }, + "fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, "fast-text-encoding": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", @@ -34512,6 +34685,18 @@ "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==" }, + "formidable": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", + "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==", + "dev": true, + "requires": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" + } + }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -35090,6 +35275,12 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", + "dev": true + }, "highlight.js": { "version": "11.8.0", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.8.0.tgz", @@ -42241,6 +42432,56 @@ } } }, + "superagent": { + "version": "8.0.9", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.9.tgz", + "integrity": "sha512-4C7Bh5pyHTvU33KpZgwrNKh/VQnvgtCSqPRfJAUdmrtSYePVzVg4E4OzsrbkhJj9O7SO6Bnv75K/F8XVZT8YHA==", + "dev": true, + "requires": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.2", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true + }, + "semver": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, "superjson": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.12.3.tgz", @@ -42250,6 +42491,16 @@ "copy-anything": "^3.0.2" } }, + "supertest": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.3.tgz", + "integrity": "sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA==", + "dev": true, + "requires": { + "methods": "^1.1.2", + "superagent": "^8.0.5" + } + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",