diff --git a/app.js b/app.js
index 9262b47..466682d 100644
--- a/app.js
+++ b/app.js
@@ -1,4 +1,5 @@
 const express = require('express');
+const rateLimit = require('express-rate-limit');
 const path = require('path');
 const bodyParser = require('body-parser');
 const API = require('./src/js/index.js');
@@ -11,7 +12,7 @@ require('./src/js/utils.js');
 app.set('trust proxy', true);
 
 // Middleware
-app.use(bodyParser.json({ limit: '10mb' }));
+// app.use(bodyParser.json({ limit: '10mb' }));
 app.use(bodyParser.urlencoded({ extended: true, limit: '10mb' }));
 app.use(express.static(__dirname));
 app.use(express.static(path.join(__dirname, 'public')));
@@ -29,6 +30,153 @@ app.use(
 
 const fs = require('fs');
 
+let DEFAULT_SSO_TOKEN = '';
+
+// Try to read the token from a file
+try {
+  const tokenPath = path.join(__dirname, 'token.txt');
+  if (fs.existsSync(tokenPath)) {
+    DEFAULT_SSO_TOKEN = fs.readFileSync(tokenPath, 'utf8').trim();
+    logger.info('Default SSO token loaded successfully from token.txt');
+  } else {
+    logger.warn('token.txt not found, demo mode may not function correctly');
+  }
+} catch (error) {
+  logger.error('Error loading token file:', { error: error.message });
+}
+
+setInterval(() => demoModeRequestTracker.cleanupExpiredEntries(), 3600000); // Clean up every hour
+
+// Configure rate limiting for demo mode
+const demoModeRequestTracker = {
+  requests: new Map(),
+  maxRequestsPerHour: 5, // Adjust as needed
+  resetInterval: 60 * 60 * 1000, // 1 hour in milliseconds
+
+  isLimited: function (ip) {
+    const now = Date.now();
+    const userRequests = this.requests.get(ip) || {
+      count: 0,
+      resetTime: now + this.resetInterval,
+    };
+
+    // Reset counter if the reset time has passed
+    if (now > userRequests.resetTime) {
+      userRequests.count = 0;
+      userRequests.resetTime = now + this.resetInterval;
+    }
+
+    return userRequests.count >= this.maxRequestsPerHour;
+  },
+
+  incrementCounter: function (ip) {
+    const now = Date.now();
+    const userRequests = this.requests.get(ip) || {
+      count: 0,
+      resetTime: now + this.resetInterval,
+    };
+
+    // Reset counter if the reset time has passed
+    if (now > userRequests.resetTime) {
+      userRequests.count = 1; // Start with 1 for this request
+      userRequests.resetTime = now + this.resetInterval;
+    } else {
+      userRequests.count++;
+    }
+
+    this.requests.set(ip, userRequests);
+    return userRequests.count;
+  },
+
+  cleanupExpiredEntries: function () {
+    const now = Date.now();
+    for (const [ip, data] of this.requests.entries()) {
+      if (now > data.resetTime) {
+        // Either remove entries completely or reset them to 0
+        this.requests.delete(ip);
+      }
+    }
+  },
+};
+
+// Demo mode middleware
+const demoModeMiddleware = (req, res, next) => {
+  // Skip non-API routes
+  if (!req.path.startsWith('/api/')) {
+    return next();
+  }
+
+  // Get client IP for tracking
+  // const clientIP = req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress;
+  const clientIP =
+    req.headers['cf-connecting-ip'] ||
+    req.headers['x-forwarded-for']?.split(',')[0] ||
+    req.ip ||
+    req.connection.remoteAddress;
+
+  // Check if demo mode is active
+  if (DEFAULT_SSO_TOKEN) {
+    // For API endpoints, check rate limit in demo mode
+    if (demoModeRequestTracker.isLimited(clientIP)) {
+      const userRequests = demoModeRequestTracker.requests.get(clientIP);
+      const resetTimeMs = userRequests.resetTime - Date.now();
+
+      // Format time as HH:MM:SS
+      const hours = Math.floor(resetTimeMs / (60 * 60 * 1000));
+      const minutes = Math.floor(
+        (resetTimeMs % (60 * 60 * 1000)) / (60 * 1000)
+      );
+      const seconds = Math.floor((resetTimeMs % (60 * 1000)) / 1000);
+      const timeFormat = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
+
+      logger.warn(`Demo mode rate limit exceeded for IP: ${clientIP}`);
+      return res.status(429).json({
+        status: 'error',
+        message: `Please try again in ${timeFormat} or use your own SSO token.`,
+        timestamp: global.Utils.toIsoString(new Date()),
+      });
+    }
+
+    // Increment the counter
+    //   const count = demoModeRequestTracker.incrementCounter(clientIP);
+    //   logger.debug(`Demo mode request count for ${clientIP}: ${count}`);
+  }
+
+  next();
+};
+
+function incrementDemoCounter(req, ssoToken) {
+  if (ssoToken === DEFAULT_SSO_TOKEN) {
+    const clientIP =
+      req.headers['cf-connecting-ip'] ||
+      req.headers['x-forwarded-for']?.split(',')[0] ||
+      req.ip ||
+      req.connection.remoteAddress;
+
+    const count = demoModeRequestTracker.incrementCounter(clientIP);
+    logger.debug(`Demo mode request count for ${clientIP}: ${count}`);
+  }
+}
+
+app.use(demoModeMiddleware);
+app.use((req, res, next) => {
+  if (req.query.demo === 'true' || req.query.demo === '1') {
+    // Enable demo mode for this session if it's not already enabled
+    if (!req.session) {
+      req.session = {};
+    }
+    req.session.forceDemoMode = true;
+    logger.info('Demo mode enabled via URL parameter');
+  } else if (req.query.demo === 'false' || req.query.demo === '0') {
+    // Disable demo mode for this session
+    if (req.session) {
+      req.session.forceDemoMode = false;
+    }
+    logger.info('Demo mode disabled via URL parameter');
+  }
+  next();
+});
+
 // Initialize key replacements
 let keyReplacements = {};
 
@@ -224,15 +372,19 @@ app.post('/api/stats', async (req, res) => {
   );
 
   try {
-    const {
-      username,
-      ssoToken,
-      platform,
-      game,
-      apiCall,
-      sanitize,
-      replaceKeys,
-    } = req.body;
+    let { username, ssoToken, platform, game, apiCall, sanitize, replaceKeys } =
+      req.body;
+
+    if (!ssoToken && DEFAULT_SSO_TOKEN) {
+      ssoToken = DEFAULT_SSO_TOKEN;
+      logger.info('Using default SSO token for demo mode');
+    } else if (!ssoToken && !DEFAULT_SSO_TOKEN) {
+      return res.status(200).json({
+        status: 'error',
+        message: 'SSO Token is required as demo mode is not active',
+        timestamp: global.Utils.toIsoString(new Date()),
+      });
+    }
 
     /*
     logger.debug(
@@ -247,10 +399,6 @@ app.post('/api/stats', async (req, res) => {
     logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`);
     logger.debug("====================="); */
 
-    if (!ssoToken) {
-      return res.status(400).json({ error: 'SSO Token is required' });
-    }
-
     // For mapList, username is not required
     if (apiCall !== 'mapList' && !username) {
       return res.status(400).json({ error: 'Username is required' });
@@ -435,6 +583,8 @@ app.post('/api/stats', async (req, res) => {
 
       const { sanitize, replaceKeys } = req.body;
 
+      incrementDemoCounter(req, ssoToken);
+
       return res.json({
         // status: "success",
         data: processJsonOutput(data, { sanitize, replaceKeys }),
@@ -475,7 +625,7 @@ app.post('/api/matches', async (req, res) => {
   );
 
   try {
-    const { username, ssoToken, platform, game, sanitize, replaceKeys } =
+    let { username, ssoToken, platform, game, sanitize, replaceKeys } =
       req.body;
 
     /*
@@ -490,10 +640,19 @@ app.post('/api/matches', async (req, res) => {
     logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`);
     logger.debug("========================"); */
 
-    if (!username || !ssoToken) {
-      return res
-        .status(400)
-        .json({ error: 'Username and SSO Token are required' });
+    if (!username) {
+      return res.status(400).json({ error: 'Username is required' });
+    }
+
+    if (!ssoToken && DEFAULT_SSO_TOKEN) {
+      ssoToken = DEFAULT_SSO_TOKEN;
+      logger.info('Using default SSO token for demo mode');
+    } else if (!ssoToken && !DEFAULT_SSO_TOKEN) {
+      return res.status(200).json({
+        status: 'error',
+        message: 'SSO Token is required as demo mode is not active',
+        timestamp: global.Utils.toIsoString(new Date()),
+      });
     }
 
     try {
@@ -584,6 +743,8 @@ app.post('/api/matches', async (req, res) => {
 
       const { sanitize, replaceKeys } = req.body;
 
+      incrementDemoCounter(req, ssoToken);
+
       return res.json({
         // status: "success",
         data: processJsonOutput(data, { sanitize, replaceKeys }),
@@ -621,9 +782,7 @@ app.post('/api/matchInfo', async (req, res) => {
   );
 
   try {
-    const { matchId, ssoToken, platform, game, sanitize, replaceKeys } =
-      req.body;
-    const mode = 'mp';
+    let { matchId, ssoToken, platform, game, sanitize, replaceKeys } = req.body;
 
     /*
     logger.debug(
@@ -637,10 +796,19 @@ app.post('/api/matchInfo', async (req, res) => {
     logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`);
     logger.debug("=========================="); */
 
-    if (!matchId || !ssoToken) {
-      return res
-        .status(400)
-        .json({ error: 'Match ID and SSO Token are required' });
+    if (!matchId) {
+      return res.status(400).json({ error: 'Match ID is required' });
+    }
+
+    if (!ssoToken && DEFAULT_SSO_TOKEN) {
+      ssoToken = DEFAULT_SSO_TOKEN;
+      logger.info('Using default SSO token for demo mode');
+    } else if (!ssoToken && !DEFAULT_SSO_TOKEN) {
+      return res.status(200).json({
+        status: 'error',
+        message: 'SSO Token is required as demo mode is not active',
+        timestamp: global.Utils.toIsoString(new Date()),
+      });
     }
 
     try {
@@ -717,6 +885,8 @@ app.post('/api/matchInfo', async (req, res) => {
 
       const { sanitize, replaceKeys } = req.body;
 
+      incrementDemoCounter(req, ssoToken);
+
       return res.json({
         // status: "success",
         data: processJsonOutput(data, { sanitize, replaceKeys }),
@@ -754,7 +924,7 @@ app.post('/api/user', async (req, res) => {
   );
 
   try {
-    const { username, ssoToken, platform, userCall, sanitize, replaceKeys } =
+    let { username, ssoToken, platform, userCall, sanitize, replaceKeys } =
       req.body;
 
     /*
@@ -769,10 +939,6 @@ app.post('/api/user', async (req, res) => {
     logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`);
     logger.debug("========================="); */
 
-    if (!ssoToken) {
-      return res.status(400).json({ error: 'SSO Token is required' });
-    }
-
     // For eventFeed and identities, username is not required
     if (
       !username &&
@@ -785,6 +951,17 @@ app.post('/api/user', async (req, res) => {
         .json({ error: 'Username is required for this API call' });
     }
 
+    if (!ssoToken && DEFAULT_SSO_TOKEN) {
+      ssoToken = DEFAULT_SSO_TOKEN;
+      logger.info('Using default SSO token for demo mode');
+    } else if (!ssoToken && !DEFAULT_SSO_TOKEN) {
+      return res.status(200).json({
+        status: 'error',
+        message: 'SSO Token is required as demo mode is not active',
+        timestamp: global.Utils.toIsoString(new Date()),
+      });
+    }
+
     try {
       await ensureLogin(ssoToken);
     } catch (loginError) {
@@ -850,6 +1027,8 @@ app.post('/api/user', async (req, res) => {
 
       const { sanitize, replaceKeys } = req.body;
 
+      incrementDemoCounter(req, ssoToken);
+
       return res.json({
         // status: "success",
         data: processJsonOutput(data, { sanitize, replaceKeys }),
@@ -886,23 +1065,33 @@ app.post('/api/search', async (req, res) => {
   );
 
   try {
-    const { username, ssoToken, platform, sanitize, replaceKeys } = req.body;
+    let { username, ssoToken, platform, sanitize, replaceKeys } = req.body;
 
     /*
     logger.debug(
-      `Request details - Username to search: ${username}, Platform: ${platform}`
+      `Request details - Match ID: ${matchId}, Platform: ${platform}, Game: ${game}`
     );
 
-    logger.debug("=== SEARCH REQUEST ===");
-    logger.debug(`Search Term: ${username}`);
+    logger.debug("=== MATCH INFO REQUEST ===");
+    logger.debug(`Match ID: ${matchId}`);
     logger.debug(`Platform: ${platform}`);
+    logger.debug(`Game: ${game}`);
     logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`);
-    logger.debug("======================"); */
+    logger.debug("=========================="); */
 
-    if (!username || !ssoToken) {
-      return res
-        .status(400)
-        .json({ error: 'Username and SSO Token are required' });
+    if (!username) {
+      return res.status(400).json({ error: 'Username is required' });
+    }
+
+    if (!ssoToken && DEFAULT_SSO_TOKEN) {
+      ssoToken = DEFAULT_SSO_TOKEN;
+      logger.info('Using default SSO token for demo mode');
+    } else if (!ssoToken && !DEFAULT_SSO_TOKEN) {
+      return res.status(200).json({
+        status: 'error',
+        message: 'SSO Token is required as demo mode is not active',
+        timestamp: global.Utils.toIsoString(new Date()),
+      });
     }
 
     try {
@@ -935,6 +1124,8 @@ app.post('/api/search', async (req, res) => {
 
       const { sanitize, replaceKeys } = req.body;
 
+      incrementDemoCounter(req, ssoToken);
+
       return res.json({
         // status: "success",
         data: processJsonOutput(data, { sanitize, replaceKeys }),
@@ -957,7 +1148,10 @@ app.post('/api/search', async (req, res) => {
 // Improved logging endpoint
 app.post('/api/log', (req, res) => {
   const clientIP =
-    req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress;
+    req.headers['cf-connecting-ip'] ||
+    req.headers['x-forwarded-for']?.split(',')[0] ||
+    req.ip ||
+    req.connection.remoteAddress;
   const userAgent = req.headers['user-agent'];
   const referer = req.headers['referer'];
   const origin = req.headers['origin'];
@@ -1028,9 +1222,32 @@ function storeLogInDatabase(logData) {
 }
 */
 
-// Basic health check endpoint
 app.get('/health', (req, res) => {
-  res.json({ status: 'ok', timestamp: global.Utils.toIsoString(new Date()) });
+  // Check if demo mode is forced via URL parameter
+  const forceDemoMode = req.session && req.session.forceDemoMode === true;
+
+  const isDemoMode = !!DEFAULT_SSO_TOKEN || forceDemoMode;
+  res.json({
+    status: 'ok',
+    timestamp: global.Utils.toIsoString(new Date()),
+    demoMode: isDemoMode,
+    requestsRemaining:
+      isDemoMode ?
+        demoModeRequestTracker.maxRequestsPerHour -
+        (demoModeRequestTracker.requests.get(req.ip)?.count || 0)
+      : null,
+  });
+});
+
+app.get('/demo', (req, res) => {
+  // Set a cookie or session variable to enable demo mode
+  if (!req.session) {
+    req.session = {};
+  }
+  req.session.forceDemoMode = true;
+
+  // Redirect to the main app
+  res.redirect('/');
 });
 
 // Serve the main HTML file
diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json
index e9c8eb3..db58e60 100644
--- a/node_modules/.package-lock.json
+++ b/node_modules/.package-lock.json
@@ -62,6 +62,15 @@
         "node": ">=6.9.0"
       }
     },
+    "node_modules/@fastify/busboy": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
+      "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=14"
+      }
+    },
     "node_modules/@isaacs/cliui": {
       "version": "8.0.2",
       "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -508,6 +517,33 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/call-of-duty-api": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/call-of-duty-api/-/call-of-duty-api-4.1.0.tgz",
+      "integrity": "sha512-f5DJ6gQru6f406QVBZkkXOv0gUzFu0hykdkyKRa2Am6iWwGRVzcBK7rq+xHpNI6oTq3EFfw0T70kb38rZaezNA==",
+      "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.4.0",
+        "undici": "^5.12.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/Lierrmm"
+      }
+    },
+    "node_modules/call-of-duty-api/node_modules/undici": {
+      "version": "5.29.0",
+      "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
+      "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==",
+      "license": "MIT",
+      "dependencies": {
+        "@fastify/busboy": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=14.0"
+      }
+    },
     "node_modules/camel-case": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz",
@@ -987,6 +1023,55 @@
         "url": "https://opencollective.com/express"
       }
     },
+    "node_modules/express-rate-limit": {
+      "version": "7.5.0",
+      "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz",
+      "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/express-rate-limit"
+      },
+      "peerDependencies": {
+        "express": "^4.11 || 5 || ^5.0.0-beta.1"
+      }
+    },
+    "node_modules/express-session": {
+      "version": "1.18.1",
+      "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz",
+      "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==",
+      "license": "MIT",
+      "dependencies": {
+        "cookie": "0.7.2",
+        "cookie-signature": "1.0.7",
+        "debug": "2.6.9",
+        "depd": "~2.0.0",
+        "on-headers": "~1.0.2",
+        "parseurl": "~1.3.3",
+        "safe-buffer": "5.2.1",
+        "uid-safe": "~2.1.5"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/express-session/node_modules/cookie": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+      "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/express-session/node_modules/cookie-signature": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+      "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+      "license": "MIT"
+    },
     "node_modules/express/node_modules/body-parser": {
       "version": "1.20.3",
       "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
@@ -1869,6 +1954,15 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/on-headers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+      "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/once": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -2045,6 +2139,21 @@
         "node": ">=10"
       }
     },
+    "node_modules/prettier": {
+      "version": "3.5.3",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
+      "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
+      "license": "MIT",
+      "bin": {
+        "prettier": "bin/prettier.cjs"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    },
     "node_modules/process-nextick-args": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -2116,6 +2225,15 @@
       ],
       "license": "MIT"
     },
+    "node_modules/random-bytes": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
+      "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/range-parser": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -2849,6 +2967,18 @@
         "node": ">=0.8.0"
       }
     },
+    "node_modules/uid-safe": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
+      "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
+      "license": "MIT",
+      "dependencies": {
+        "random-bytes": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/undici": {
       "version": "7.6.0",
       "resolved": "https://registry.npmjs.org/undici/-/undici-7.6.0.tgz",
diff --git a/node_modules/@fastify/busboy/deps/dicer/lib/Dicer.js b/node_modules/@fastify/busboy/deps/dicer/lib/Dicer.js
new file mode 100644
index 0000000..3c8c081
--- /dev/null
+++ b/node_modules/@fastify/busboy/deps/dicer/lib/Dicer.js
@@ -0,0 +1,213 @@
+'use strict'
+
+const WritableStream = require('node:stream').Writable
+const inherits = require('node:util').inherits
+
+const StreamSearch = require('../../streamsearch/sbmh')
+
+const PartStream = require('./PartStream')
+const HeaderParser = require('./HeaderParser')
+
+const DASH = 45
+const B_ONEDASH = Buffer.from('-')
+const B_CRLF = Buffer.from('\r\n')
+const EMPTY_FN = function () {}
+
+function Dicer (cfg) {
+  if (!(this instanceof Dicer)) { return new Dicer(cfg) }
+  WritableStream.call(this, cfg)
+
+  if (!cfg || (!cfg.headerFirst && typeof cfg.boundary !== 'string')) { throw new TypeError('Boundary required') }
+
+  if (typeof cfg.boundary === 'string') { this.setBoundary(cfg.boundary) } else { this._bparser = undefined }
+
+  this._headerFirst = cfg.headerFirst
+
+  this._dashes = 0
+  this._parts = 0
+  this._finished = false
+  this._realFinish = false
+  this._isPreamble = true
+  this._justMatched = false
+  this._firstWrite = true
+  this._inHeader = true
+  this._part = undefined
+  this._cb = undefined
+  this._ignoreData = false
+  this._partOpts = { highWaterMark: cfg.partHwm }
+  this._pause = false
+
+  const self = this
+  this._hparser = new HeaderParser(cfg)
+  this._hparser.on('header', function (header) {
+    self._inHeader = false
+    self._part.emit('header', header)
+  })
+}
+inherits(Dicer, WritableStream)
+
+Dicer.prototype.emit = function (ev) {
+  if (ev === 'finish' && !this._realFinish) {
+    if (!this._finished) {
+      const self = this
+      process.nextTick(function () {
+        self.emit('error', new Error('Unexpected end of multipart data'))
+        if (self._part && !self._ignoreData) {
+          const type = (self._isPreamble ? 'Preamble' : 'Part')
+          self._part.emit('error', new Error(type + ' terminated early due to unexpected end of multipart data'))
+          self._part.push(null)
+          process.nextTick(function () {
+            self._realFinish = true
+            self.emit('finish')
+            self._realFinish = false
+          })
+          return
+        }
+        self._realFinish = true
+        self.emit('finish')
+        self._realFinish = false
+      })
+    }
+  } else { WritableStream.prototype.emit.apply(this, arguments) }
+}
+
+Dicer.prototype._write = function (data, encoding, cb) {
+  // ignore unexpected data (e.g. extra trailer data after finished)
+  if (!this._hparser && !this._bparser) { return cb() }
+
+  if (this._headerFirst && this._isPreamble) {
+    if (!this._part) {
+      this._part = new PartStream(this._partOpts)
+      if (this.listenerCount('preamble') !== 0) { this.emit('preamble', this._part) } else { this._ignore() }
+    }
+    const r = this._hparser.push(data)
+    if (!this._inHeader && r !== undefined && r < data.length) { data = data.slice(r) } else { return cb() }
+  }
+
+  // allows for "easier" testing
+  if (this._firstWrite) {
+    this._bparser.push(B_CRLF)
+    this._firstWrite = false
+  }
+
+  this._bparser.push(data)
+
+  if (this._pause) { this._cb = cb } else { cb() }
+}
+
+Dicer.prototype.reset = function () {
+  this._part = undefined
+  this._bparser = undefined
+  this._hparser = undefined
+}
+
+Dicer.prototype.setBoundary = function (boundary) {
+  const self = this
+  this._bparser = new StreamSearch('\r\n--' + boundary)
+  this._bparser.on('info', function (isMatch, data, start, end) {
+    self._oninfo(isMatch, data, start, end)
+  })
+}
+
+Dicer.prototype._ignore = function () {
+  if (this._part && !this._ignoreData) {
+    this._ignoreData = true
+    this._part.on('error', EMPTY_FN)
+    // we must perform some kind of read on the stream even though we are
+    // ignoring the data, otherwise node's Readable stream will not emit 'end'
+    // after pushing null to the stream
+    this._part.resume()
+  }
+}
+
+Dicer.prototype._oninfo = function (isMatch, data, start, end) {
+  let buf; const self = this; let i = 0; let r; let shouldWriteMore = true
+
+  if (!this._part && this._justMatched && data) {
+    while (this._dashes < 2 && (start + i) < end) {
+      if (data[start + i] === DASH) {
+        ++i
+        ++this._dashes
+      } else {
+        if (this._dashes) { buf = B_ONEDASH }
+        this._dashes = 0
+        break
+      }
+    }
+    if (this._dashes === 2) {
+      if ((start + i) < end && this.listenerCount('trailer') !== 0) { this.emit('trailer', data.slice(start + i, end)) }
+      this.reset()
+      this._finished = true
+      // no more parts will be added
+      if (self._parts === 0) {
+        self._realFinish = true
+        self.emit('finish')
+        self._realFinish = false
+      }
+    }
+    if (this._dashes) { return }
+  }
+  if (this._justMatched) { this._justMatched = false }
+  if (!this._part) {
+    this._part = new PartStream(this._partOpts)
+    this._part._read = function (n) {
+      self._unpause()
+    }
+    if (this._isPreamble && this.listenerCount('preamble') !== 0) {
+      this.emit('preamble', this._part)
+    } else if (this._isPreamble !== true && this.listenerCount('part') !== 0) {
+      this.emit('part', this._part)
+    } else {
+      this._ignore()
+    }
+    if (!this._isPreamble) { this._inHeader = true }
+  }
+  if (data && start < end && !this._ignoreData) {
+    if (this._isPreamble || !this._inHeader) {
+      if (buf) { shouldWriteMore = this._part.push(buf) }
+      shouldWriteMore = this._part.push(data.slice(start, end))
+      if (!shouldWriteMore) { this._pause = true }
+    } else if (!this._isPreamble && this._inHeader) {
+      if (buf) { this._hparser.push(buf) }
+      r = this._hparser.push(data.slice(start, end))
+      if (!this._inHeader && r !== undefined && r < end) { this._oninfo(false, data, start + r, end) }
+    }
+  }
+  if (isMatch) {
+    this._hparser.reset()
+    if (this._isPreamble) { this._isPreamble = false } else {
+      if (start !== end) {
+        ++this._parts
+        this._part.on('end', function () {
+          if (--self._parts === 0) {
+            if (self._finished) {
+              self._realFinish = true
+              self.emit('finish')
+              self._realFinish = false
+            } else {
+              self._unpause()
+            }
+          }
+        })
+      }
+    }
+    this._part.push(null)
+    this._part = undefined
+    this._ignoreData = false
+    this._justMatched = true
+    this._dashes = 0
+  }
+}
+
+Dicer.prototype._unpause = function () {
+  if (!this._pause) { return }
+
+  this._pause = false
+  if (this._cb) {
+    const cb = this._cb
+    this._cb = undefined
+    cb()
+  }
+}
+
+module.exports = Dicer
diff --git a/node_modules/@fastify/busboy/deps/dicer/lib/HeaderParser.js b/node_modules/@fastify/busboy/deps/dicer/lib/HeaderParser.js
new file mode 100644
index 0000000..65f667b
--- /dev/null
+++ b/node_modules/@fastify/busboy/deps/dicer/lib/HeaderParser.js
@@ -0,0 +1,100 @@
+'use strict'
+
+const EventEmitter = require('node:events').EventEmitter
+const inherits = require('node:util').inherits
+const getLimit = require('../../../lib/utils/getLimit')
+
+const StreamSearch = require('../../streamsearch/sbmh')
+
+const B_DCRLF = Buffer.from('\r\n\r\n')
+const RE_CRLF = /\r\n/g
+const RE_HDR = /^([^:]+):[ \t]?([\x00-\xFF]+)?$/ // eslint-disable-line no-control-regex
+
+function HeaderParser (cfg) {
+  EventEmitter.call(this)
+
+  cfg = cfg || {}
+  const self = this
+  this.nread = 0
+  this.maxed = false
+  this.npairs = 0
+  this.maxHeaderPairs = getLimit(cfg, 'maxHeaderPairs', 2000)
+  this.maxHeaderSize = getLimit(cfg, 'maxHeaderSize', 80 * 1024)
+  this.buffer = ''
+  this.header = {}
+  this.finished = false
+  this.ss = new StreamSearch(B_DCRLF)
+  this.ss.on('info', function (isMatch, data, start, end) {
+    if (data && !self.maxed) {
+      if (self.nread + end - start >= self.maxHeaderSize) {
+        end = self.maxHeaderSize - self.nread + start
+        self.nread = self.maxHeaderSize
+        self.maxed = true
+      } else { self.nread += (end - start) }
+
+      self.buffer += data.toString('binary', start, end)
+    }
+    if (isMatch) { self._finish() }
+  })
+}
+inherits(HeaderParser, EventEmitter)
+
+HeaderParser.prototype.push = function (data) {
+  const r = this.ss.push(data)
+  if (this.finished) { return r }
+}
+
+HeaderParser.prototype.reset = function () {
+  this.finished = false
+  this.buffer = ''
+  this.header = {}
+  this.ss.reset()
+}
+
+HeaderParser.prototype._finish = function () {
+  if (this.buffer) { this._parseHeader() }
+  this.ss.matches = this.ss.maxMatches
+  const header = this.header
+  this.header = {}
+  this.buffer = ''
+  this.finished = true
+  this.nread = this.npairs = 0
+  this.maxed = false
+  this.emit('header', header)
+}
+
+HeaderParser.prototype._parseHeader = function () {
+  if (this.npairs === this.maxHeaderPairs) { return }
+
+  const lines = this.buffer.split(RE_CRLF)
+  const len = lines.length
+  let m, h
+
+  for (var i = 0; i < len; ++i) { // eslint-disable-line no-var
+    if (lines[i].length === 0) { continue }
+    if (lines[i][0] === '\t' || lines[i][0] === ' ') {
+      // folded header content
+      // RFC2822 says to just remove the CRLF and not the whitespace following
+      // it, so we follow the RFC and include the leading whitespace ...
+      if (h) {
+        this.header[h][this.header[h].length - 1] += lines[i]
+        continue
+      }
+    }
+
+    const posColon = lines[i].indexOf(':')
+    if (
+      posColon === -1 ||
+      posColon === 0
+    ) {
+      return
+    }
+    m = RE_HDR.exec(lines[i])
+    h = m[1].toLowerCase()
+    this.header[h] = this.header[h] || []
+    this.header[h].push((m[2] || ''))
+    if (++this.npairs === this.maxHeaderPairs) { break }
+  }
+}
+
+module.exports = HeaderParser
diff --git a/node_modules/@fastify/busboy/deps/dicer/lib/PartStream.js b/node_modules/@fastify/busboy/deps/dicer/lib/PartStream.js
new file mode 100644
index 0000000..c91da1c
--- /dev/null
+++ b/node_modules/@fastify/busboy/deps/dicer/lib/PartStream.js
@@ -0,0 +1,13 @@
+'use strict'
+
+const inherits = require('node:util').inherits
+const ReadableStream = require('node:stream').Readable
+
+function PartStream (opts) {
+  ReadableStream.call(this, opts)
+}
+inherits(PartStream, ReadableStream)
+
+PartStream.prototype._read = function (n) {}
+
+module.exports = PartStream
diff --git a/node_modules/@fastify/busboy/deps/dicer/lib/dicer.d.ts b/node_modules/@fastify/busboy/deps/dicer/lib/dicer.d.ts
new file mode 100644
index 0000000..3c5b896
--- /dev/null
+++ b/node_modules/@fastify/busboy/deps/dicer/lib/dicer.d.ts
@@ -0,0 +1,164 @@
+// Type definitions for dicer 0.2
+// Project: https://github.com/mscdex/dicer
+// Definitions by: BendingBender <https://github.com/BendingBender>
+// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
+// TypeScript Version: 2.2
+/// <reference types="node" />
+
+import stream = require("stream");
+
+// tslint:disable:unified-signatures
+
+/**
+ * A very fast streaming multipart parser for node.js.
+ * Dicer is a WritableStream
+ *
+ * Dicer (special) events:
+ * - on('finish', ()) - Emitted when all parts have been parsed and the Dicer instance has been ended.
+ * - on('part', (stream: PartStream)) - Emitted when a new part has been found.
+ * - on('preamble', (stream: PartStream)) - Emitted for preamble if you should happen to need it (can usually be ignored).
+ * - on('trailer', (data: Buffer)) - Emitted when trailing data was found after the terminating boundary (as with the preamble, this can usually be ignored too).
+ */
+export class Dicer extends stream.Writable {
+    /**
+     * Creates and returns a new Dicer instance with the following valid config settings:
+     *
+     * @param config The configuration to use
+     */
+    constructor(config: Dicer.Config);
+    /**
+     * Sets the boundary to use for parsing and performs some initialization needed for parsing.
+     * You should only need to use this if you set headerFirst to true in the constructor and are parsing the boundary from the preamble header.
+     *
+     * @param boundary The boundary to use
+     */
+    setBoundary(boundary: string): void;
+    addListener(event: "finish", listener: () => void): this;
+    addListener(event: "part", listener: (stream: Dicer.PartStream) => void): this;
+    addListener(event: "preamble", listener: (stream: Dicer.PartStream) => void): this;
+    addListener(event: "trailer", listener: (data: Buffer) => void): this;
+    addListener(event: "close", listener: () => void): this;
+    addListener(event: "drain", listener: () => void): this;
+    addListener(event: "error", listener: (err: Error) => void): this;
+    addListener(event: "pipe", listener: (src: stream.Readable) => void): this;
+    addListener(event: "unpipe", listener: (src: stream.Readable) => void): this;
+    addListener(event: string, listener: (...args: any[]) => void): this;
+    on(event: "finish", listener: () => void): this;
+    on(event: "part", listener: (stream: Dicer.PartStream) => void): this;
+    on(event: "preamble", listener: (stream: Dicer.PartStream) => void): this;
+    on(event: "trailer", listener: (data: Buffer) => void): this;
+    on(event: "close", listener: () => void): this;
+    on(event: "drain", listener: () => void): this;
+    on(event: "error", listener: (err: Error) => void): this;
+    on(event: "pipe", listener: (src: stream.Readable) => void): this;
+    on(event: "unpipe", listener: (src: stream.Readable) => void): this;
+    on(event: string, listener: (...args: any[]) => void): this;
+    once(event: "finish", listener: () => void): this;
+    once(event: "part", listener: (stream: Dicer.PartStream) => void): this;
+    once(event: "preamble", listener: (stream: Dicer.PartStream) => void): this;
+    once(event: "trailer", listener: (data: Buffer) => void): this;
+    once(event: "close", listener: () => void): this;
+    once(event: "drain", listener: () => void): this;
+    once(event: "error", listener: (err: Error) => void): this;
+    once(event: "pipe", listener: (src: stream.Readable) => void): this;
+    once(event: "unpipe", listener: (src: stream.Readable) => void): this;
+    once(event: string, listener: (...args: any[]) => void): this;
+    prependListener(event: "finish", listener: () => void): this;
+    prependListener(event: "part", listener: (stream: Dicer.PartStream) => void): this;
+    prependListener(event: "preamble", listener: (stream: Dicer.PartStream) => void): this;
+    prependListener(event: "trailer", listener: (data: Buffer) => void): this;
+    prependListener(event: "close", listener: () => void): this;
+    prependListener(event: "drain", listener: () => void): this;
+    prependListener(event: "error", listener: (err: Error) => void): this;
+    prependListener(event: "pipe", listener: (src: stream.Readable) => void): this;
+    prependListener(event: "unpipe", listener: (src: stream.Readable) => void): this;
+    prependListener(event: string, listener: (...args: any[]) => void): this;
+    prependOnceListener(event: "finish", listener: () => void): this;
+    prependOnceListener(event: "part", listener: (stream: Dicer.PartStream) => void): this;
+    prependOnceListener(event: "preamble", listener: (stream: Dicer.PartStream) => void): this;
+    prependOnceListener(event: "trailer", listener: (data: Buffer) => void): this;
+    prependOnceListener(event: "close", listener: () => void): this;
+    prependOnceListener(event: "drain", listener: () => void): this;
+    prependOnceListener(event: "error", listener: (err: Error) => void): this;
+    prependOnceListener(event: "pipe", listener: (src: stream.Readable) => void): this;
+    prependOnceListener(event: "unpipe", listener: (src: stream.Readable) => void): this;
+    prependOnceListener(event: string, listener: (...args: any[]) => void): this;
+    removeListener(event: "finish", listener: () => void): this;
+    removeListener(event: "part", listener: (stream: Dicer.PartStream) => void): this;
+    removeListener(event: "preamble", listener: (stream: Dicer.PartStream) => void): this;
+    removeListener(event: "trailer", listener: (data: Buffer) => void): this;
+    removeListener(event: "close", listener: () => void): this;
+    removeListener(event: "drain", listener: () => void): this;
+    removeListener(event: "error", listener: (err: Error) => void): this;
+    removeListener(event: "pipe", listener: (src: stream.Readable) => void): this;
+    removeListener(event: "unpipe", listener: (src: stream.Readable) => void): this;
+    removeListener(event: string, listener: (...args: any[]) => void): this;
+}
+
+declare namespace Dicer {
+    interface Config {
+        /**
+         * This is the boundary used to detect the beginning of a new part.
+         */
+        boundary?: string | undefined;
+        /**
+         * If true, preamble header parsing will be performed first.
+         */
+        headerFirst?: boolean | undefined;
+        /**
+         * The maximum number of header key=>value pairs to parse Default: 2000 (same as node's http).
+         */
+        maxHeaderPairs?: number | undefined;
+    }
+
+    /**
+     * PartStream is a _ReadableStream_
+     *
+     * PartStream (special) events:
+     * - on('header', (header: object)) - An object containing the header for this particular part. Each property value is an array of one or more string values.
+     */
+    interface PartStream extends stream.Readable {
+        addListener(event: "header", listener: (header: object) => void): this;
+        addListener(event: "close", listener: () => void): this;
+        addListener(event: "data", listener: (chunk: Buffer | string) => void): this;
+        addListener(event: "end", listener: () => void): this;
+        addListener(event: "readable", listener: () => void): this;
+        addListener(event: "error", listener: (err: Error) => void): this;
+        addListener(event: string, listener: (...args: any[]) => void): this;
+        on(event: "header", listener: (header: object) => void): this;
+        on(event: "close", listener: () => void): this;
+        on(event: "data", listener: (chunk: Buffer | string) => void): this;
+        on(event: "end", listener: () => void): this;
+        on(event: "readable", listener: () => void): this;
+        on(event: "error", listener: (err: Error) => void): this;
+        on(event: string, listener: (...args: any[]) => void): this;
+        once(event: "header", listener: (header: object) => void): this;
+        once(event: "close", listener: () => void): this;
+        once(event: "data", listener: (chunk: Buffer | string) => void): this;
+        once(event: "end", listener: () => void): this;
+        once(event: "readable", listener: () => void): this;
+        once(event: "error", listener: (err: Error) => void): this;
+        once(event: string, listener: (...args: any[]) => void): this;
+        prependListener(event: "header", listener: (header: object) => void): this;
+        prependListener(event: "close", listener: () => void): this;
+        prependListener(event: "data", listener: (chunk: Buffer | string) => void): this;
+        prependListener(event: "end", listener: () => void): this;
+        prependListener(event: "readable", listener: () => void): this;
+        prependListener(event: "error", listener: (err: Error) => void): this;
+        prependListener(event: string, listener: (...args: any[]) => void): this;
+        prependOnceListener(event: "header", listener: (header: object) => void): this;
+        prependOnceListener(event: "close", listener: () => void): this;
+        prependOnceListener(event: "data", listener: (chunk: Buffer | string) => void): this;
+        prependOnceListener(event: "end", listener: () => void): this;
+        prependOnceListener(event: "readable", listener: () => void): this;
+        prependOnceListener(event: "error", listener: (err: Error) => void): this;
+        prependOnceListener(event: string, listener: (...args: any[]) => void): this;
+        removeListener(event: "header", listener: (header: object) => void): this;
+        removeListener(event: "close", listener: () => void): this;
+        removeListener(event: "data", listener: (chunk: Buffer | string) => void): this;
+        removeListener(event: "end", listener: () => void): this;
+        removeListener(event: "readable", listener: () => void): this;
+        removeListener(event: "error", listener: (err: Error) => void): this;
+        removeListener(event: string, listener: (...args: any[]) => void): this;
+    }
+}
\ No newline at end of file
diff --git a/node_modules/@fastify/busboy/deps/streamsearch/sbmh.js b/node_modules/@fastify/busboy/deps/streamsearch/sbmh.js
new file mode 100644
index 0000000..b90c0e8
--- /dev/null
+++ b/node_modules/@fastify/busboy/deps/streamsearch/sbmh.js
@@ -0,0 +1,228 @@
+'use strict'
+
+/**
+ * Copyright Brian White. All rights reserved.
+ *
+ * @see https://github.com/mscdex/streamsearch
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ *
+ * Based heavily on the Streaming Boyer-Moore-Horspool C++ implementation
+ * by Hongli Lai at: https://github.com/FooBarWidget/boyer-moore-horspool
+ */
+const EventEmitter = require('node:events').EventEmitter
+const inherits = require('node:util').inherits
+
+function SBMH (needle) {
+  if (typeof needle === 'string') {
+    needle = Buffer.from(needle)
+  }
+
+  if (!Buffer.isBuffer(needle)) {
+    throw new TypeError('The needle has to be a String or a Buffer.')
+  }
+
+  const needleLength = needle.length
+
+  if (needleLength === 0) {
+    throw new Error('The needle cannot be an empty String/Buffer.')
+  }
+
+  if (needleLength > 256) {
+    throw new Error('The needle cannot have a length bigger than 256.')
+  }
+
+  this.maxMatches = Infinity
+  this.matches = 0
+
+  this._occ = new Array(256)
+    .fill(needleLength) // Initialize occurrence table.
+  this._lookbehind_size = 0
+  this._needle = needle
+  this._bufpos = 0
+
+  this._lookbehind = Buffer.alloc(needleLength)
+
+  // Populate occurrence table with analysis of the needle,
+  // ignoring last letter.
+  for (var i = 0; i < needleLength - 1; ++i) { // eslint-disable-line no-var
+    this._occ[needle[i]] = needleLength - 1 - i
+  }
+}
+inherits(SBMH, EventEmitter)
+
+SBMH.prototype.reset = function () {
+  this._lookbehind_size = 0
+  this.matches = 0
+  this._bufpos = 0
+}
+
+SBMH.prototype.push = function (chunk, pos) {
+  if (!Buffer.isBuffer(chunk)) {
+    chunk = Buffer.from(chunk, 'binary')
+  }
+  const chlen = chunk.length
+  this._bufpos = pos || 0
+  let r
+  while (r !== chlen && this.matches < this.maxMatches) { r = this._sbmh_feed(chunk) }
+  return r
+}
+
+SBMH.prototype._sbmh_feed = function (data) {
+  const len = data.length
+  const needle = this._needle
+  const needleLength = needle.length
+  const lastNeedleChar = needle[needleLength - 1]
+
+  // Positive: points to a position in `data`
+  //           pos == 3 points to data[3]
+  // Negative: points to a position in the lookbehind buffer
+  //           pos == -2 points to lookbehind[lookbehind_size - 2]
+  let pos = -this._lookbehind_size
+  let ch
+
+  if (pos < 0) {
+    // Lookbehind buffer is not empty. Perform Boyer-Moore-Horspool
+    // search with character lookup code that considers both the
+    // lookbehind buffer and the current round's haystack data.
+    //
+    // Loop until
+    //   there is a match.
+    // or until
+    //   we've moved past the position that requires the
+    //   lookbehind buffer. In this case we switch to the
+    //   optimized loop.
+    // or until
+    //   the character to look at lies outside the haystack.
+    while (pos < 0 && pos <= len - needleLength) {
+      ch = this._sbmh_lookup_char(data, pos + needleLength - 1)
+
+      if (
+        ch === lastNeedleChar &&
+        this._sbmh_memcmp(data, pos, needleLength - 1)
+      ) {
+        this._lookbehind_size = 0
+        ++this.matches
+        this.emit('info', true)
+
+        return (this._bufpos = pos + needleLength)
+      }
+      pos += this._occ[ch]
+    }
+
+    // No match.
+
+    if (pos < 0) {
+      // There's too few data for Boyer-Moore-Horspool to run,
+      // so let's use a different algorithm to skip as much as
+      // we can.
+      // Forward pos until
+      //   the trailing part of lookbehind + data
+      //   looks like the beginning of the needle
+      // or until
+      //   pos == 0
+      while (pos < 0 && !this._sbmh_memcmp(data, pos, len - pos)) { ++pos }
+    }
+
+    if (pos >= 0) {
+      // Discard lookbehind buffer.
+      this.emit('info', false, this._lookbehind, 0, this._lookbehind_size)
+      this._lookbehind_size = 0
+    } else {
+      // Cut off part of the lookbehind buffer that has
+      // been processed and append the entire haystack
+      // into it.
+      const bytesToCutOff = this._lookbehind_size + pos
+      if (bytesToCutOff > 0) {
+        // The cut off data is guaranteed not to contain the needle.
+        this.emit('info', false, this._lookbehind, 0, bytesToCutOff)
+      }
+
+      this._lookbehind.copy(this._lookbehind, 0, bytesToCutOff,
+        this._lookbehind_size - bytesToCutOff)
+      this._lookbehind_size -= bytesToCutOff
+
+      data.copy(this._lookbehind, this._lookbehind_size)
+      this._lookbehind_size += len
+
+      this._bufpos = len
+      return len
+    }
+  }
+
+  pos += (pos >= 0) * this._bufpos
+
+  // Lookbehind buffer is now empty. We only need to check if the
+  // needle is in the haystack.
+  if (data.indexOf(needle, pos) !== -1) {
+    pos = data.indexOf(needle, pos)
+    ++this.matches
+    if (pos > 0) { this.emit('info', true, data, this._bufpos, pos) } else { this.emit('info', true) }
+
+    return (this._bufpos = pos + needleLength)
+  } else {
+    pos = len - needleLength
+  }
+
+  // There was no match. If there's trailing haystack data that we cannot
+  // match yet using the Boyer-Moore-Horspool algorithm (because the trailing
+  // data is less than the needle size) then match using a modified
+  // algorithm that starts matching from the beginning instead of the end.
+  // Whatever trailing data is left after running this algorithm is added to
+  // the lookbehind buffer.
+  while (
+    pos < len &&
+    (
+      data[pos] !== needle[0] ||
+      (
+        (Buffer.compare(
+          data.subarray(pos, pos + len - pos),
+          needle.subarray(0, len - pos)
+        ) !== 0)
+      )
+    )
+  ) {
+    ++pos
+  }
+  if (pos < len) {
+    data.copy(this._lookbehind, 0, pos, pos + (len - pos))
+    this._lookbehind_size = len - pos
+  }
+
+  // Everything until pos is guaranteed not to contain needle data.
+  if (pos > 0) { this.emit('info', false, data, this._bufpos, pos < len ? pos : len) }
+
+  this._bufpos = len
+  return len
+}
+
+SBMH.prototype._sbmh_lookup_char = function (data, pos) {
+  return (pos < 0)
+    ? this._lookbehind[this._lookbehind_size + pos]
+    : data[pos]
+}
+
+SBMH.prototype._sbmh_memcmp = function (data, pos, len) {
+  for (var i = 0; i < len; ++i) { // eslint-disable-line no-var
+    if (this._sbmh_lookup_char(data, pos + i) !== this._needle[i]) { return false }
+  }
+  return true
+}
+
+module.exports = SBMH
diff --git a/node_modules/@fastify/busboy/lib/main.d.ts b/node_modules/@fastify/busboy/lib/main.d.ts
new file mode 100644
index 0000000..91b6448
--- /dev/null
+++ b/node_modules/@fastify/busboy/lib/main.d.ts
@@ -0,0 +1,196 @@
+// Definitions by: Jacob Baskin <https://github.com/jacobbaskin>
+//                 BendingBender <https://github.com/BendingBender>
+//                 Igor Savin <https://github.com/kibertoad>
+
+/// <reference types="node" />
+
+import * as http from 'http';
+import { Readable, Writable } from 'stream';
+export { Dicer } from "../deps/dicer/lib/dicer";
+
+export const Busboy: BusboyConstructor;
+export default Busboy;
+
+export interface BusboyConfig {
+    /**
+     * These are the HTTP headers of the incoming request, which are used by individual parsers.
+     */
+    headers: BusboyHeaders;
+    /**
+     * `highWaterMark` to use for this Busboy instance.
+     * @default WritableStream default.
+     */
+    highWaterMark?: number | undefined;
+    /**
+     * highWaterMark to use for file streams.
+     * @default ReadableStream default.
+     */
+    fileHwm?: number | undefined;
+    /**
+     * Default character set to use when one isn't defined.
+     * @default 'utf8'
+     */
+    defCharset?: string | undefined;
+    /**
+     * Detect if a Part is a file.
+     * 
+     * By default a file is detected if contentType 
+     * is application/octet-stream or fileName is not
+     * undefined.
+     * 
+     * Modify this to handle e.g. Blobs.
+     */
+    isPartAFile?: (fieldName: string | undefined, contentType: string | undefined, fileName: string | undefined) => boolean;
+    /**
+     * If paths in the multipart 'filename' field shall be preserved.
+     * @default false
+     */
+    preservePath?: boolean | undefined;
+    /**
+     * Various limits on incoming data.
+     */
+    limits?:
+    | {
+        /**
+         * Max field name size (in bytes)
+         * @default 100 bytes
+         */
+        fieldNameSize?: number | undefined;
+        /**
+         * Max field value size (in bytes)
+         * @default 1MB
+         */
+        fieldSize?: number | undefined;
+        /**
+         * Max number of non-file fields
+         * @default Infinity
+         */
+        fields?: number | undefined;
+        /**
+         * For multipart forms, the max file size (in bytes)
+         * @default Infinity
+         */
+        fileSize?: number | undefined;
+        /**
+         * For multipart forms, the max number of file fields
+         * @default Infinity
+         */
+        files?: number | undefined;
+        /**
+         * For multipart forms, the max number of parts (fields + files)
+         * @default Infinity
+         */
+        parts?: number | undefined;
+        /**
+         * For multipart forms, the max number of header key=>value pairs to parse
+         * @default 2000
+         */
+        headerPairs?: number | undefined;
+
+        /**
+         * For multipart forms, the max size of a header part
+         * @default 81920
+         */
+        headerSize?: number | undefined;
+    }
+    | undefined;
+}
+
+export type BusboyHeaders = { 'content-type': string } & http.IncomingHttpHeaders;
+
+export interface BusboyFileStream extends
+    Readable {
+
+        truncated: boolean;
+
+        /**
+         * The number of bytes that have been read so far.
+         */
+        bytesRead: number;
+}
+
+export interface Busboy extends Writable {
+    addListener<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
+
+    addListener(event: string | symbol, listener: (...args: any[]) => void): this;
+
+    on<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
+
+    on(event: string | symbol, listener: (...args: any[]) => void): this;
+
+    once<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
+
+    once(event: string | symbol, listener: (...args: any[]) => void): this;
+
+    removeListener<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
+
+    removeListener(event: string | symbol, listener: (...args: any[]) => void): this;
+
+    off<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
+
+    off(event: string | symbol, listener: (...args: any[]) => void): this;
+
+    prependListener<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
+
+    prependListener(event: string | symbol, listener: (...args: any[]) => void): this;
+
+    prependOnceListener<Event extends keyof BusboyEvents>(event: Event, listener: BusboyEvents[Event]): this;
+
+    prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this;
+}
+
+export interface BusboyEvents {
+    /**
+     * Emitted for each new file form field found.
+     *
+     * * Note: if you listen for this event, you should always handle the `stream` no matter if you care about the
+     * file contents or not (e.g. you can simply just do `stream.resume();` if you want to discard the contents),
+     * otherwise the 'finish' event will never fire on the Busboy instance. However, if you don't care about **any**
+     * incoming files, you can simply not listen for the 'file' event at all and any/all files will be automatically
+     * and safely discarded (these discarded files do still count towards `files` and `parts` limits).
+     * * If a configured file size limit was reached, `stream` will both have a boolean property `truncated`
+     * (best checked at the end of the stream) and emit a 'limit' event to notify you when this happens.
+     *
+     * @param listener.transferEncoding Contains the 'Content-Transfer-Encoding' value for the file stream.
+     * @param listener.mimeType Contains the 'Content-Type' value for the file stream.
+     */
+    file: (
+        fieldname: string,
+        stream: BusboyFileStream,
+        filename: string,
+        transferEncoding: string,
+        mimeType: string,
+    ) => void;
+    /**
+     * Emitted for each new non-file field found.
+     */
+    field: (
+        fieldname: string,
+        value: string,
+        fieldnameTruncated: boolean,
+        valueTruncated: boolean,
+        transferEncoding: string,
+        mimeType: string,
+    ) => void;
+    finish: () => void;
+    /**
+     * Emitted when specified `parts` limit has been reached. No more 'file' or 'field' events will be emitted.
+     */
+    partsLimit: () => void;
+    /**
+     *  Emitted when specified `files` limit has been reached. No more 'file' events will be emitted.
+     */
+    filesLimit: () => void;
+    /**
+     * Emitted when specified `fields` limit has been reached. No more 'field' events will be emitted.
+     */
+    fieldsLimit: () => void;
+    error: (error: unknown) => void;
+}
+
+export interface BusboyConstructor {
+    (options: BusboyConfig): Busboy;
+
+    new(options: BusboyConfig): Busboy;
+}
+
diff --git a/node_modules/@fastify/busboy/lib/main.js b/node_modules/@fastify/busboy/lib/main.js
new file mode 100644
index 0000000..8794beb
--- /dev/null
+++ b/node_modules/@fastify/busboy/lib/main.js
@@ -0,0 +1,85 @@
+'use strict'
+
+const WritableStream = require('node:stream').Writable
+const { inherits } = require('node:util')
+const Dicer = require('../deps/dicer/lib/Dicer')
+
+const MultipartParser = require('./types/multipart')
+const UrlencodedParser = require('./types/urlencoded')
+const parseParams = require('./utils/parseParams')
+
+function Busboy (opts) {
+  if (!(this instanceof Busboy)) { return new Busboy(opts) }
+
+  if (typeof opts !== 'object') {
+    throw new TypeError('Busboy expected an options-Object.')
+  }
+  if (typeof opts.headers !== 'object') {
+    throw new TypeError('Busboy expected an options-Object with headers-attribute.')
+  }
+  if (typeof opts.headers['content-type'] !== 'string') {
+    throw new TypeError('Missing Content-Type-header.')
+  }
+
+  const {
+    headers,
+    ...streamOptions
+  } = opts
+
+  this.opts = {
+    autoDestroy: false,
+    ...streamOptions
+  }
+  WritableStream.call(this, this.opts)
+
+  this._done = false
+  this._parser = this.getParserByHeaders(headers)
+  this._finished = false
+}
+inherits(Busboy, WritableStream)
+
+Busboy.prototype.emit = function (ev) {
+  if (ev === 'finish') {
+    if (!this._done) {
+      this._parser?.end()
+      return
+    } else if (this._finished) {
+      return
+    }
+    this._finished = true
+  }
+  WritableStream.prototype.emit.apply(this, arguments)
+}
+
+Busboy.prototype.getParserByHeaders = function (headers) {
+  const parsed = parseParams(headers['content-type'])
+
+  const cfg = {
+    defCharset: this.opts.defCharset,
+    fileHwm: this.opts.fileHwm,
+    headers,
+    highWaterMark: this.opts.highWaterMark,
+    isPartAFile: this.opts.isPartAFile,
+    limits: this.opts.limits,
+    parsedConType: parsed,
+    preservePath: this.opts.preservePath
+  }
+
+  if (MultipartParser.detect.test(parsed[0])) {
+    return new MultipartParser(this, cfg)
+  }
+  if (UrlencodedParser.detect.test(parsed[0])) {
+    return new UrlencodedParser(this, cfg)
+  }
+  throw new Error('Unsupported Content-Type.')
+}
+
+Busboy.prototype._write = function (chunk, encoding, cb) {
+  this._parser.write(chunk, cb)
+}
+
+module.exports = Busboy
+module.exports.default = Busboy
+module.exports.Busboy = Busboy
+
+module.exports.Dicer = Dicer
diff --git a/node_modules/@fastify/busboy/lib/types/multipart.js b/node_modules/@fastify/busboy/lib/types/multipart.js
new file mode 100644
index 0000000..d691eca
--- /dev/null
+++ b/node_modules/@fastify/busboy/lib/types/multipart.js
@@ -0,0 +1,306 @@
+'use strict'
+
+// TODO:
+//  * support 1 nested multipart level
+//    (see second multipart example here:
+//     http://www.w3.org/TR/html401/interact/forms.html#didx-multipartform-data)
+//  * support limits.fieldNameSize
+//     -- this will require modifications to utils.parseParams
+
+const { Readable } = require('node:stream')
+const { inherits } = require('node:util')
+
+const Dicer = require('../../deps/dicer/lib/Dicer')
+
+const parseParams = require('../utils/parseParams')
+const decodeText = require('../utils/decodeText')
+const basename = require('../utils/basename')
+const getLimit = require('../utils/getLimit')
+
+const RE_BOUNDARY = /^boundary$/i
+const RE_FIELD = /^form-data$/i
+const RE_CHARSET = /^charset$/i
+const RE_FILENAME = /^filename$/i
+const RE_NAME = /^name$/i
+
+Multipart.detect = /^multipart\/form-data/i
+function Multipart (boy, cfg) {
+  let i
+  let len
+  const self = this
+  let boundary
+  const limits = cfg.limits
+  const isPartAFile = cfg.isPartAFile || ((fieldName, contentType, fileName) => (contentType === 'application/octet-stream' || fileName !== undefined))
+  const parsedConType = cfg.parsedConType || []
+  const defCharset = cfg.defCharset || 'utf8'
+  const preservePath = cfg.preservePath
+  const fileOpts = { highWaterMark: cfg.fileHwm }
+
+  for (i = 0, len = parsedConType.length; i < len; ++i) {
+    if (Array.isArray(parsedConType[i]) &&
+      RE_BOUNDARY.test(parsedConType[i][0])) {
+      boundary = parsedConType[i][1]
+      break
+    }
+  }
+
+  function checkFinished () {
+    if (nends === 0 && finished && !boy._done) {
+      finished = false
+      self.end()
+    }
+  }
+
+  if (typeof boundary !== 'string') { throw new Error('Multipart: Boundary not found') }
+
+  const fieldSizeLimit = getLimit(limits, 'fieldSize', 1 * 1024 * 1024)
+  const fileSizeLimit = getLimit(limits, 'fileSize', Infinity)
+  const filesLimit = getLimit(limits, 'files', Infinity)
+  const fieldsLimit = getLimit(limits, 'fields', Infinity)
+  const partsLimit = getLimit(limits, 'parts', Infinity)
+  const headerPairsLimit = getLimit(limits, 'headerPairs', 2000)
+  const headerSizeLimit = getLimit(limits, 'headerSize', 80 * 1024)
+
+  let nfiles = 0
+  let nfields = 0
+  let nends = 0
+  let curFile
+  let curField
+  let finished = false
+
+  this._needDrain = false
+  this._pause = false
+  this._cb = undefined
+  this._nparts = 0
+  this._boy = boy
+
+  const parserCfg = {
+    boundary,
+    maxHeaderPairs: headerPairsLimit,
+    maxHeaderSize: headerSizeLimit,
+    partHwm: fileOpts.highWaterMark,
+    highWaterMark: cfg.highWaterMark
+  }
+
+  this.parser = new Dicer(parserCfg)
+  this.parser.on('drain', function () {
+    self._needDrain = false
+    if (self._cb && !self._pause) {
+      const cb = self._cb
+      self._cb = undefined
+      cb()
+    }
+  }).on('part', function onPart (part) {
+    if (++self._nparts > partsLimit) {
+      self.parser.removeListener('part', onPart)
+      self.parser.on('part', skipPart)
+      boy.hitPartsLimit = true
+      boy.emit('partsLimit')
+      return skipPart(part)
+    }
+
+    // hack because streams2 _always_ doesn't emit 'end' until nextTick, so let
+    // us emit 'end' early since we know the part has ended if we are already
+    // seeing the next part
+    if (curField) {
+      const field = curField
+      field.emit('end')
+      field.removeAllListeners('end')
+    }
+
+    part.on('header', function (header) {
+      let contype
+      let fieldname
+      let parsed
+      let charset
+      let encoding
+      let filename
+      let nsize = 0
+
+      if (header['content-type']) {
+        parsed = parseParams(header['content-type'][0])
+        if (parsed[0]) {
+          contype = parsed[0].toLowerCase()
+          for (i = 0, len = parsed.length; i < len; ++i) {
+            if (RE_CHARSET.test(parsed[i][0])) {
+              charset = parsed[i][1].toLowerCase()
+              break
+            }
+          }
+        }
+      }
+
+      if (contype === undefined) { contype = 'text/plain' }
+      if (charset === undefined) { charset = defCharset }
+
+      if (header['content-disposition']) {
+        parsed = parseParams(header['content-disposition'][0])
+        if (!RE_FIELD.test(parsed[0])) { return skipPart(part) }
+        for (i = 0, len = parsed.length; i < len; ++i) {
+          if (RE_NAME.test(parsed[i][0])) {
+            fieldname = parsed[i][1]
+          } else if (RE_FILENAME.test(parsed[i][0])) {
+            filename = parsed[i][1]
+            if (!preservePath) { filename = basename(filename) }
+          }
+        }
+      } else { return skipPart(part) }
+
+      if (header['content-transfer-encoding']) { encoding = header['content-transfer-encoding'][0].toLowerCase() } else { encoding = '7bit' }
+
+      let onData,
+        onEnd
+
+      if (isPartAFile(fieldname, contype, filename)) {
+        // file/binary field
+        if (nfiles === filesLimit) {
+          if (!boy.hitFilesLimit) {
+            boy.hitFilesLimit = true
+            boy.emit('filesLimit')
+          }
+          return skipPart(part)
+        }
+
+        ++nfiles
+
+        if (boy.listenerCount('file') === 0) {
+          self.parser._ignore()
+          return
+        }
+
+        ++nends
+        const file = new FileStream(fileOpts)
+        curFile = file
+        file.on('end', function () {
+          --nends
+          self._pause = false
+          checkFinished()
+          if (self._cb && !self._needDrain) {
+            const cb = self._cb
+            self._cb = undefined
+            cb()
+          }
+        })
+        file._read = function (n) {
+          if (!self._pause) { return }
+          self._pause = false
+          if (self._cb && !self._needDrain) {
+            const cb = self._cb
+            self._cb = undefined
+            cb()
+          }
+        }
+        boy.emit('file', fieldname, file, filename, encoding, contype)
+
+        onData = function (data) {
+          if ((nsize += data.length) > fileSizeLimit) {
+            const extralen = fileSizeLimit - nsize + data.length
+            if (extralen > 0) { file.push(data.slice(0, extralen)) }
+            file.truncated = true
+            file.bytesRead = fileSizeLimit
+            part.removeAllListeners('data')
+            file.emit('limit')
+            return
+          } else if (!file.push(data)) { self._pause = true }
+
+          file.bytesRead = nsize
+        }
+
+        onEnd = function () {
+          curFile = undefined
+          file.push(null)
+        }
+      } else {
+        // non-file field
+        if (nfields === fieldsLimit) {
+          if (!boy.hitFieldsLimit) {
+            boy.hitFieldsLimit = true
+            boy.emit('fieldsLimit')
+          }
+          return skipPart(part)
+        }
+
+        ++nfields
+        ++nends
+        let buffer = ''
+        let truncated = false
+        curField = part
+
+        onData = function (data) {
+          if ((nsize += data.length) > fieldSizeLimit) {
+            const extralen = (fieldSizeLimit - (nsize - data.length))
+            buffer += data.toString('binary', 0, extralen)
+            truncated = true
+            part.removeAllListeners('data')
+          } else { buffer += data.toString('binary') }
+        }
+
+        onEnd = function () {
+          curField = undefined
+          if (buffer.length) { buffer = decodeText(buffer, 'binary', charset) }
+          boy.emit('field', fieldname, buffer, false, truncated, encoding, contype)
+          --nends
+          checkFinished()
+        }
+      }
+
+      /* As of node@2efe4ab761666 (v0.10.29+/v0.11.14+), busboy had become
+         broken. Streams2/streams3 is a huge black box of confusion, but
+         somehow overriding the sync state seems to fix things again (and still
+         seems to work for previous node versions).
+      */
+      part._readableState.sync = false
+
+      part.on('data', onData)
+      part.on('end', onEnd)
+    }).on('error', function (err) {
+      if (curFile) { curFile.emit('error', err) }
+    })
+  }).on('error', function (err) {
+    boy.emit('error', err)
+  }).on('finish', function () {
+    finished = true
+    checkFinished()
+  })
+}
+
+Multipart.prototype.write = function (chunk, cb) {
+  const r = this.parser.write(chunk)
+  if (r && !this._pause) {
+    cb()
+  } else {
+    this._needDrain = !r
+    this._cb = cb
+  }
+}
+
+Multipart.prototype.end = function () {
+  const self = this
+
+  if (self.parser.writable) {
+    self.parser.end()
+  } else if (!self._boy._done) {
+    process.nextTick(function () {
+      self._boy._done = true
+      self._boy.emit('finish')
+    })
+  }
+}
+
+function skipPart (part) {
+  part.resume()
+}
+
+function FileStream (opts) {
+  Readable.call(this, opts)
+
+  this.bytesRead = 0
+
+  this.truncated = false
+}
+
+inherits(FileStream, Readable)
+
+FileStream.prototype._read = function (n) {}
+
+module.exports = Multipart
diff --git a/node_modules/@fastify/busboy/lib/types/urlencoded.js b/node_modules/@fastify/busboy/lib/types/urlencoded.js
new file mode 100644
index 0000000..6f5f784
--- /dev/null
+++ b/node_modules/@fastify/busboy/lib/types/urlencoded.js
@@ -0,0 +1,190 @@
+'use strict'
+
+const Decoder = require('../utils/Decoder')
+const decodeText = require('../utils/decodeText')
+const getLimit = require('../utils/getLimit')
+
+const RE_CHARSET = /^charset$/i
+
+UrlEncoded.detect = /^application\/x-www-form-urlencoded/i
+function UrlEncoded (boy, cfg) {
+  const limits = cfg.limits
+  const parsedConType = cfg.parsedConType
+  this.boy = boy
+
+  this.fieldSizeLimit = getLimit(limits, 'fieldSize', 1 * 1024 * 1024)
+  this.fieldNameSizeLimit = getLimit(limits, 'fieldNameSize', 100)
+  this.fieldsLimit = getLimit(limits, 'fields', Infinity)
+
+  let charset
+  for (var i = 0, len = parsedConType.length; i < len; ++i) { // eslint-disable-line no-var
+    if (Array.isArray(parsedConType[i]) &&
+        RE_CHARSET.test(parsedConType[i][0])) {
+      charset = parsedConType[i][1].toLowerCase()
+      break
+    }
+  }
+
+  if (charset === undefined) { charset = cfg.defCharset || 'utf8' }
+
+  this.decoder = new Decoder()
+  this.charset = charset
+  this._fields = 0
+  this._state = 'key'
+  this._checkingBytes = true
+  this._bytesKey = 0
+  this._bytesVal = 0
+  this._key = ''
+  this._val = ''
+  this._keyTrunc = false
+  this._valTrunc = false
+  this._hitLimit = false
+}
+
+UrlEncoded.prototype.write = function (data, cb) {
+  if (this._fields === this.fieldsLimit) {
+    if (!this.boy.hitFieldsLimit) {
+      this.boy.hitFieldsLimit = true
+      this.boy.emit('fieldsLimit')
+    }
+    return cb()
+  }
+
+  let idxeq; let idxamp; let i; let p = 0; const len = data.length
+
+  while (p < len) {
+    if (this._state === 'key') {
+      idxeq = idxamp = undefined
+      for (i = p; i < len; ++i) {
+        if (!this._checkingBytes) { ++p }
+        if (data[i] === 0x3D/* = */) {
+          idxeq = i
+          break
+        } else if (data[i] === 0x26/* & */) {
+          idxamp = i
+          break
+        }
+        if (this._checkingBytes && this._bytesKey === this.fieldNameSizeLimit) {
+          this._hitLimit = true
+          break
+        } else if (this._checkingBytes) { ++this._bytesKey }
+      }
+
+      if (idxeq !== undefined) {
+        // key with assignment
+        if (idxeq > p) { this._key += this.decoder.write(data.toString('binary', p, idxeq)) }
+        this._state = 'val'
+
+        this._hitLimit = false
+        this._checkingBytes = true
+        this._val = ''
+        this._bytesVal = 0
+        this._valTrunc = false
+        this.decoder.reset()
+
+        p = idxeq + 1
+      } else if (idxamp !== undefined) {
+        // key with no assignment
+        ++this._fields
+        let key; const keyTrunc = this._keyTrunc
+        if (idxamp > p) { key = (this._key += this.decoder.write(data.toString('binary', p, idxamp))) } else { key = this._key }
+
+        this._hitLimit = false
+        this._checkingBytes = true
+        this._key = ''
+        this._bytesKey = 0
+        this._keyTrunc = false
+        this.decoder.reset()
+
+        if (key.length) {
+          this.boy.emit('field', decodeText(key, 'binary', this.charset),
+            '',
+            keyTrunc,
+            false)
+        }
+
+        p = idxamp + 1
+        if (this._fields === this.fieldsLimit) { return cb() }
+      } else if (this._hitLimit) {
+        // we may not have hit the actual limit if there are encoded bytes...
+        if (i > p) { this._key += this.decoder.write(data.toString('binary', p, i)) }
+        p = i
+        if ((this._bytesKey = this._key.length) === this.fieldNameSizeLimit) {
+          // yep, we actually did hit the limit
+          this._checkingBytes = false
+          this._keyTrunc = true
+        }
+      } else {
+        if (p < len) { this._key += this.decoder.write(data.toString('binary', p)) }
+        p = len
+      }
+    } else {
+      idxamp = undefined
+      for (i = p; i < len; ++i) {
+        if (!this._checkingBytes) { ++p }
+        if (data[i] === 0x26/* & */) {
+          idxamp = i
+          break
+        }
+        if (this._checkingBytes && this._bytesVal === this.fieldSizeLimit) {
+          this._hitLimit = true
+          break
+        } else if (this._checkingBytes) { ++this._bytesVal }
+      }
+
+      if (idxamp !== undefined) {
+        ++this._fields
+        if (idxamp > p) { this._val += this.decoder.write(data.toString('binary', p, idxamp)) }
+        this.boy.emit('field', decodeText(this._key, 'binary', this.charset),
+          decodeText(this._val, 'binary', this.charset),
+          this._keyTrunc,
+          this._valTrunc)
+        this._state = 'key'
+
+        this._hitLimit = false
+        this._checkingBytes = true
+        this._key = ''
+        this._bytesKey = 0
+        this._keyTrunc = false
+        this.decoder.reset()
+
+        p = idxamp + 1
+        if (this._fields === this.fieldsLimit) { return cb() }
+      } else if (this._hitLimit) {
+        // we may not have hit the actual limit if there are encoded bytes...
+        if (i > p) { this._val += this.decoder.write(data.toString('binary', p, i)) }
+        p = i
+        if ((this._val === '' && this.fieldSizeLimit === 0) ||
+            (this._bytesVal = this._val.length) === this.fieldSizeLimit) {
+          // yep, we actually did hit the limit
+          this._checkingBytes = false
+          this._valTrunc = true
+        }
+      } else {
+        if (p < len) { this._val += this.decoder.write(data.toString('binary', p)) }
+        p = len
+      }
+    }
+  }
+  cb()
+}
+
+UrlEncoded.prototype.end = function () {
+  if (this.boy._done) { return }
+
+  if (this._state === 'key' && this._key.length > 0) {
+    this.boy.emit('field', decodeText(this._key, 'binary', this.charset),
+      '',
+      this._keyTrunc,
+      false)
+  } else if (this._state === 'val') {
+    this.boy.emit('field', decodeText(this._key, 'binary', this.charset),
+      decodeText(this._val, 'binary', this.charset),
+      this._keyTrunc,
+      this._valTrunc)
+  }
+  this.boy._done = true
+  this.boy.emit('finish')
+}
+
+module.exports = UrlEncoded
diff --git a/node_modules/@fastify/busboy/lib/utils/Decoder.js b/node_modules/@fastify/busboy/lib/utils/Decoder.js
new file mode 100644
index 0000000..7917678
--- /dev/null
+++ b/node_modules/@fastify/busboy/lib/utils/Decoder.js
@@ -0,0 +1,54 @@
+'use strict'
+
+const RE_PLUS = /\+/g
+
+const HEX = [
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
+  0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+]
+
+function Decoder () {
+  this.buffer = undefined
+}
+Decoder.prototype.write = function (str) {
+  // Replace '+' with ' ' before decoding
+  str = str.replace(RE_PLUS, ' ')
+  let res = ''
+  let i = 0; let p = 0; const len = str.length
+  for (; i < len; ++i) {
+    if (this.buffer !== undefined) {
+      if (!HEX[str.charCodeAt(i)]) {
+        res += '%' + this.buffer
+        this.buffer = undefined
+        --i // retry character
+      } else {
+        this.buffer += str[i]
+        ++p
+        if (this.buffer.length === 2) {
+          res += String.fromCharCode(parseInt(this.buffer, 16))
+          this.buffer = undefined
+        }
+      }
+    } else if (str[i] === '%') {
+      if (i > p) {
+        res += str.substring(p, i)
+        p = i
+      }
+      this.buffer = ''
+      ++p
+    }
+  }
+  if (p < len && this.buffer === undefined) { res += str.substring(p) }
+  return res
+}
+Decoder.prototype.reset = function () {
+  this.buffer = undefined
+}
+
+module.exports = Decoder
diff --git a/node_modules/@fastify/busboy/lib/utils/basename.js b/node_modules/@fastify/busboy/lib/utils/basename.js
new file mode 100644
index 0000000..db58819
--- /dev/null
+++ b/node_modules/@fastify/busboy/lib/utils/basename.js
@@ -0,0 +1,14 @@
+'use strict'
+
+module.exports = function basename (path) {
+  if (typeof path !== 'string') { return '' }
+  for (var i = path.length - 1; i >= 0; --i) { // eslint-disable-line no-var
+    switch (path.charCodeAt(i)) {
+      case 0x2F: // '/'
+      case 0x5C: // '\'
+        path = path.slice(i + 1)
+        return (path === '..' || path === '.' ? '' : path)
+    }
+  }
+  return (path === '..' || path === '.' ? '' : path)
+}
diff --git a/node_modules/@fastify/busboy/lib/utils/decodeText.js b/node_modules/@fastify/busboy/lib/utils/decodeText.js
new file mode 100644
index 0000000..eac7d35
--- /dev/null
+++ b/node_modules/@fastify/busboy/lib/utils/decodeText.js
@@ -0,0 +1,114 @@
+'use strict'
+
+// Node has always utf-8
+const utf8Decoder = new TextDecoder('utf-8')
+const textDecoders = new Map([
+  ['utf-8', utf8Decoder],
+  ['utf8', utf8Decoder]
+])
+
+function getDecoder (charset) {
+  let lc
+  while (true) {
+    switch (charset) {
+      case 'utf-8':
+      case 'utf8':
+        return decoders.utf8
+      case 'latin1':
+      case 'ascii': // TODO: Make these a separate, strict decoder?
+      case 'us-ascii':
+      case 'iso-8859-1':
+      case 'iso8859-1':
+      case 'iso88591':
+      case 'iso_8859-1':
+      case 'windows-1252':
+      case 'iso_8859-1:1987':
+      case 'cp1252':
+      case 'x-cp1252':
+        return decoders.latin1
+      case 'utf16le':
+      case 'utf-16le':
+      case 'ucs2':
+      case 'ucs-2':
+        return decoders.utf16le
+      case 'base64':
+        return decoders.base64
+      default:
+        if (lc === undefined) {
+          lc = true
+          charset = charset.toLowerCase()
+          continue
+        }
+        return decoders.other.bind(charset)
+    }
+  }
+}
+
+const decoders = {
+  utf8: (data, sourceEncoding) => {
+    if (data.length === 0) {
+      return ''
+    }
+    if (typeof data === 'string') {
+      data = Buffer.from(data, sourceEncoding)
+    }
+    return data.utf8Slice(0, data.length)
+  },
+
+  latin1: (data, sourceEncoding) => {
+    if (data.length === 0) {
+      return ''
+    }
+    if (typeof data === 'string') {
+      return data
+    }
+    return data.latin1Slice(0, data.length)
+  },
+
+  utf16le: (data, sourceEncoding) => {
+    if (data.length === 0) {
+      return ''
+    }
+    if (typeof data === 'string') {
+      data = Buffer.from(data, sourceEncoding)
+    }
+    return data.ucs2Slice(0, data.length)
+  },
+
+  base64: (data, sourceEncoding) => {
+    if (data.length === 0) {
+      return ''
+    }
+    if (typeof data === 'string') {
+      data = Buffer.from(data, sourceEncoding)
+    }
+    return data.base64Slice(0, data.length)
+  },
+
+  other: (data, sourceEncoding) => {
+    if (data.length === 0) {
+      return ''
+    }
+    if (typeof data === 'string') {
+      data = Buffer.from(data, sourceEncoding)
+    }
+
+    if (textDecoders.has(this.toString())) {
+      try {
+        return textDecoders.get(this).decode(data)
+      } catch {}
+    }
+    return typeof data === 'string'
+      ? data
+      : data.toString()
+  }
+}
+
+function decodeText (text, sourceEncoding, destEncoding) {
+  if (text) {
+    return getDecoder(destEncoding)(text, sourceEncoding)
+  }
+  return text
+}
+
+module.exports = decodeText
diff --git a/node_modules/@fastify/busboy/lib/utils/getLimit.js b/node_modules/@fastify/busboy/lib/utils/getLimit.js
new file mode 100644
index 0000000..cb64fd6
--- /dev/null
+++ b/node_modules/@fastify/busboy/lib/utils/getLimit.js
@@ -0,0 +1,16 @@
+'use strict'
+
+module.exports = function getLimit (limits, name, defaultLimit) {
+  if (
+    !limits ||
+    limits[name] === undefined ||
+    limits[name] === null
+  ) { return defaultLimit }
+
+  if (
+    typeof limits[name] !== 'number' ||
+    isNaN(limits[name])
+  ) { throw new TypeError('Limit ' + name + ' is not a valid number') }
+
+  return limits[name]
+}
diff --git a/node_modules/@fastify/busboy/lib/utils/parseParams.js b/node_modules/@fastify/busboy/lib/utils/parseParams.js
new file mode 100644
index 0000000..1698e62
--- /dev/null
+++ b/node_modules/@fastify/busboy/lib/utils/parseParams.js
@@ -0,0 +1,196 @@
+/* eslint-disable object-property-newline */
+'use strict'
+
+const decodeText = require('./decodeText')
+
+const RE_ENCODED = /%[a-fA-F0-9][a-fA-F0-9]/g
+
+const EncodedLookup = {
+  '%00': '\x00', '%01': '\x01', '%02': '\x02', '%03': '\x03', '%04': '\x04',
+  '%05': '\x05', '%06': '\x06', '%07': '\x07', '%08': '\x08', '%09': '\x09',
+  '%0a': '\x0a', '%0A': '\x0a', '%0b': '\x0b', '%0B': '\x0b', '%0c': '\x0c',
+  '%0C': '\x0c', '%0d': '\x0d', '%0D': '\x0d', '%0e': '\x0e', '%0E': '\x0e',
+  '%0f': '\x0f', '%0F': '\x0f', '%10': '\x10', '%11': '\x11', '%12': '\x12',
+  '%13': '\x13', '%14': '\x14', '%15': '\x15', '%16': '\x16', '%17': '\x17',
+  '%18': '\x18', '%19': '\x19', '%1a': '\x1a', '%1A': '\x1a', '%1b': '\x1b',
+  '%1B': '\x1b', '%1c': '\x1c', '%1C': '\x1c', '%1d': '\x1d', '%1D': '\x1d',
+  '%1e': '\x1e', '%1E': '\x1e', '%1f': '\x1f', '%1F': '\x1f', '%20': '\x20',
+  '%21': '\x21', '%22': '\x22', '%23': '\x23', '%24': '\x24', '%25': '\x25',
+  '%26': '\x26', '%27': '\x27', '%28': '\x28', '%29': '\x29', '%2a': '\x2a',
+  '%2A': '\x2a', '%2b': '\x2b', '%2B': '\x2b', '%2c': '\x2c', '%2C': '\x2c',
+  '%2d': '\x2d', '%2D': '\x2d', '%2e': '\x2e', '%2E': '\x2e', '%2f': '\x2f',
+  '%2F': '\x2f', '%30': '\x30', '%31': '\x31', '%32': '\x32', '%33': '\x33',
+  '%34': '\x34', '%35': '\x35', '%36': '\x36', '%37': '\x37', '%38': '\x38',
+  '%39': '\x39', '%3a': '\x3a', '%3A': '\x3a', '%3b': '\x3b', '%3B': '\x3b',
+  '%3c': '\x3c', '%3C': '\x3c', '%3d': '\x3d', '%3D': '\x3d', '%3e': '\x3e',
+  '%3E': '\x3e', '%3f': '\x3f', '%3F': '\x3f', '%40': '\x40', '%41': '\x41',
+  '%42': '\x42', '%43': '\x43', '%44': '\x44', '%45': '\x45', '%46': '\x46',
+  '%47': '\x47', '%48': '\x48', '%49': '\x49', '%4a': '\x4a', '%4A': '\x4a',
+  '%4b': '\x4b', '%4B': '\x4b', '%4c': '\x4c', '%4C': '\x4c', '%4d': '\x4d',
+  '%4D': '\x4d', '%4e': '\x4e', '%4E': '\x4e', '%4f': '\x4f', '%4F': '\x4f',
+  '%50': '\x50', '%51': '\x51', '%52': '\x52', '%53': '\x53', '%54': '\x54',
+  '%55': '\x55', '%56': '\x56', '%57': '\x57', '%58': '\x58', '%59': '\x59',
+  '%5a': '\x5a', '%5A': '\x5a', '%5b': '\x5b', '%5B': '\x5b', '%5c': '\x5c',
+  '%5C': '\x5c', '%5d': '\x5d', '%5D': '\x5d', '%5e': '\x5e', '%5E': '\x5e',
+  '%5f': '\x5f', '%5F': '\x5f', '%60': '\x60', '%61': '\x61', '%62': '\x62',
+  '%63': '\x63', '%64': '\x64', '%65': '\x65', '%66': '\x66', '%67': '\x67',
+  '%68': '\x68', '%69': '\x69', '%6a': '\x6a', '%6A': '\x6a', '%6b': '\x6b',
+  '%6B': '\x6b', '%6c': '\x6c', '%6C': '\x6c', '%6d': '\x6d', '%6D': '\x6d',
+  '%6e': '\x6e', '%6E': '\x6e', '%6f': '\x6f', '%6F': '\x6f', '%70': '\x70',
+  '%71': '\x71', '%72': '\x72', '%73': '\x73', '%74': '\x74', '%75': '\x75',
+  '%76': '\x76', '%77': '\x77', '%78': '\x78', '%79': '\x79', '%7a': '\x7a',
+  '%7A': '\x7a', '%7b': '\x7b', '%7B': '\x7b', '%7c': '\x7c', '%7C': '\x7c',
+  '%7d': '\x7d', '%7D': '\x7d', '%7e': '\x7e', '%7E': '\x7e', '%7f': '\x7f',
+  '%7F': '\x7f', '%80': '\x80', '%81': '\x81', '%82': '\x82', '%83': '\x83',
+  '%84': '\x84', '%85': '\x85', '%86': '\x86', '%87': '\x87', '%88': '\x88',
+  '%89': '\x89', '%8a': '\x8a', '%8A': '\x8a', '%8b': '\x8b', '%8B': '\x8b',
+  '%8c': '\x8c', '%8C': '\x8c', '%8d': '\x8d', '%8D': '\x8d', '%8e': '\x8e',
+  '%8E': '\x8e', '%8f': '\x8f', '%8F': '\x8f', '%90': '\x90', '%91': '\x91',
+  '%92': '\x92', '%93': '\x93', '%94': '\x94', '%95': '\x95', '%96': '\x96',
+  '%97': '\x97', '%98': '\x98', '%99': '\x99', '%9a': '\x9a', '%9A': '\x9a',
+  '%9b': '\x9b', '%9B': '\x9b', '%9c': '\x9c', '%9C': '\x9c', '%9d': '\x9d',
+  '%9D': '\x9d', '%9e': '\x9e', '%9E': '\x9e', '%9f': '\x9f', '%9F': '\x9f',
+  '%a0': '\xa0', '%A0': '\xa0', '%a1': '\xa1', '%A1': '\xa1', '%a2': '\xa2',
+  '%A2': '\xa2', '%a3': '\xa3', '%A3': '\xa3', '%a4': '\xa4', '%A4': '\xa4',
+  '%a5': '\xa5', '%A5': '\xa5', '%a6': '\xa6', '%A6': '\xa6', '%a7': '\xa7',
+  '%A7': '\xa7', '%a8': '\xa8', '%A8': '\xa8', '%a9': '\xa9', '%A9': '\xa9',
+  '%aa': '\xaa', '%Aa': '\xaa', '%aA': '\xaa', '%AA': '\xaa', '%ab': '\xab',
+  '%Ab': '\xab', '%aB': '\xab', '%AB': '\xab', '%ac': '\xac', '%Ac': '\xac',
+  '%aC': '\xac', '%AC': '\xac', '%ad': '\xad', '%Ad': '\xad', '%aD': '\xad',
+  '%AD': '\xad', '%ae': '\xae', '%Ae': '\xae', '%aE': '\xae', '%AE': '\xae',
+  '%af': '\xaf', '%Af': '\xaf', '%aF': '\xaf', '%AF': '\xaf', '%b0': '\xb0',
+  '%B0': '\xb0', '%b1': '\xb1', '%B1': '\xb1', '%b2': '\xb2', '%B2': '\xb2',
+  '%b3': '\xb3', '%B3': '\xb3', '%b4': '\xb4', '%B4': '\xb4', '%b5': '\xb5',
+  '%B5': '\xb5', '%b6': '\xb6', '%B6': '\xb6', '%b7': '\xb7', '%B7': '\xb7',
+  '%b8': '\xb8', '%B8': '\xb8', '%b9': '\xb9', '%B9': '\xb9', '%ba': '\xba',
+  '%Ba': '\xba', '%bA': '\xba', '%BA': '\xba', '%bb': '\xbb', '%Bb': '\xbb',
+  '%bB': '\xbb', '%BB': '\xbb', '%bc': '\xbc', '%Bc': '\xbc', '%bC': '\xbc',
+  '%BC': '\xbc', '%bd': '\xbd', '%Bd': '\xbd', '%bD': '\xbd', '%BD': '\xbd',
+  '%be': '\xbe', '%Be': '\xbe', '%bE': '\xbe', '%BE': '\xbe', '%bf': '\xbf',
+  '%Bf': '\xbf', '%bF': '\xbf', '%BF': '\xbf', '%c0': '\xc0', '%C0': '\xc0',
+  '%c1': '\xc1', '%C1': '\xc1', '%c2': '\xc2', '%C2': '\xc2', '%c3': '\xc3',
+  '%C3': '\xc3', '%c4': '\xc4', '%C4': '\xc4', '%c5': '\xc5', '%C5': '\xc5',
+  '%c6': '\xc6', '%C6': '\xc6', '%c7': '\xc7', '%C7': '\xc7', '%c8': '\xc8',
+  '%C8': '\xc8', '%c9': '\xc9', '%C9': '\xc9', '%ca': '\xca', '%Ca': '\xca',
+  '%cA': '\xca', '%CA': '\xca', '%cb': '\xcb', '%Cb': '\xcb', '%cB': '\xcb',
+  '%CB': '\xcb', '%cc': '\xcc', '%Cc': '\xcc', '%cC': '\xcc', '%CC': '\xcc',
+  '%cd': '\xcd', '%Cd': '\xcd', '%cD': '\xcd', '%CD': '\xcd', '%ce': '\xce',
+  '%Ce': '\xce', '%cE': '\xce', '%CE': '\xce', '%cf': '\xcf', '%Cf': '\xcf',
+  '%cF': '\xcf', '%CF': '\xcf', '%d0': '\xd0', '%D0': '\xd0', '%d1': '\xd1',
+  '%D1': '\xd1', '%d2': '\xd2', '%D2': '\xd2', '%d3': '\xd3', '%D3': '\xd3',
+  '%d4': '\xd4', '%D4': '\xd4', '%d5': '\xd5', '%D5': '\xd5', '%d6': '\xd6',
+  '%D6': '\xd6', '%d7': '\xd7', '%D7': '\xd7', '%d8': '\xd8', '%D8': '\xd8',
+  '%d9': '\xd9', '%D9': '\xd9', '%da': '\xda', '%Da': '\xda', '%dA': '\xda',
+  '%DA': '\xda', '%db': '\xdb', '%Db': '\xdb', '%dB': '\xdb', '%DB': '\xdb',
+  '%dc': '\xdc', '%Dc': '\xdc', '%dC': '\xdc', '%DC': '\xdc', '%dd': '\xdd',
+  '%Dd': '\xdd', '%dD': '\xdd', '%DD': '\xdd', '%de': '\xde', '%De': '\xde',
+  '%dE': '\xde', '%DE': '\xde', '%df': '\xdf', '%Df': '\xdf', '%dF': '\xdf',
+  '%DF': '\xdf', '%e0': '\xe0', '%E0': '\xe0', '%e1': '\xe1', '%E1': '\xe1',
+  '%e2': '\xe2', '%E2': '\xe2', '%e3': '\xe3', '%E3': '\xe3', '%e4': '\xe4',
+  '%E4': '\xe4', '%e5': '\xe5', '%E5': '\xe5', '%e6': '\xe6', '%E6': '\xe6',
+  '%e7': '\xe7', '%E7': '\xe7', '%e8': '\xe8', '%E8': '\xe8', '%e9': '\xe9',
+  '%E9': '\xe9', '%ea': '\xea', '%Ea': '\xea', '%eA': '\xea', '%EA': '\xea',
+  '%eb': '\xeb', '%Eb': '\xeb', '%eB': '\xeb', '%EB': '\xeb', '%ec': '\xec',
+  '%Ec': '\xec', '%eC': '\xec', '%EC': '\xec', '%ed': '\xed', '%Ed': '\xed',
+  '%eD': '\xed', '%ED': '\xed', '%ee': '\xee', '%Ee': '\xee', '%eE': '\xee',
+  '%EE': '\xee', '%ef': '\xef', '%Ef': '\xef', '%eF': '\xef', '%EF': '\xef',
+  '%f0': '\xf0', '%F0': '\xf0', '%f1': '\xf1', '%F1': '\xf1', '%f2': '\xf2',
+  '%F2': '\xf2', '%f3': '\xf3', '%F3': '\xf3', '%f4': '\xf4', '%F4': '\xf4',
+  '%f5': '\xf5', '%F5': '\xf5', '%f6': '\xf6', '%F6': '\xf6', '%f7': '\xf7',
+  '%F7': '\xf7', '%f8': '\xf8', '%F8': '\xf8', '%f9': '\xf9', '%F9': '\xf9',
+  '%fa': '\xfa', '%Fa': '\xfa', '%fA': '\xfa', '%FA': '\xfa', '%fb': '\xfb',
+  '%Fb': '\xfb', '%fB': '\xfb', '%FB': '\xfb', '%fc': '\xfc', '%Fc': '\xfc',
+  '%fC': '\xfc', '%FC': '\xfc', '%fd': '\xfd', '%Fd': '\xfd', '%fD': '\xfd',
+  '%FD': '\xfd', '%fe': '\xfe', '%Fe': '\xfe', '%fE': '\xfe', '%FE': '\xfe',
+  '%ff': '\xff', '%Ff': '\xff', '%fF': '\xff', '%FF': '\xff'
+}
+
+function encodedReplacer (match) {
+  return EncodedLookup[match]
+}
+
+const STATE_KEY = 0
+const STATE_VALUE = 1
+const STATE_CHARSET = 2
+const STATE_LANG = 3
+
+function parseParams (str) {
+  const res = []
+  let state = STATE_KEY
+  let charset = ''
+  let inquote = false
+  let escaping = false
+  let p = 0
+  let tmp = ''
+  const len = str.length
+
+  for (var i = 0; i < len; ++i) { // eslint-disable-line no-var
+    const char = str[i]
+    if (char === '\\' && inquote) {
+      if (escaping) { escaping = false } else {
+        escaping = true
+        continue
+      }
+    } else if (char === '"') {
+      if (!escaping) {
+        if (inquote) {
+          inquote = false
+          state = STATE_KEY
+        } else { inquote = true }
+        continue
+      } else { escaping = false }
+    } else {
+      if (escaping && inquote) { tmp += '\\' }
+      escaping = false
+      if ((state === STATE_CHARSET || state === STATE_LANG) && char === "'") {
+        if (state === STATE_CHARSET) {
+          state = STATE_LANG
+          charset = tmp.substring(1)
+        } else { state = STATE_VALUE }
+        tmp = ''
+        continue
+      } else if (state === STATE_KEY &&
+        (char === '*' || char === '=') &&
+        res.length) {
+        state = char === '*'
+          ? STATE_CHARSET
+          : STATE_VALUE
+        res[p] = [tmp, undefined]
+        tmp = ''
+        continue
+      } else if (!inquote && char === ';') {
+        state = STATE_KEY
+        if (charset) {
+          if (tmp.length) {
+            tmp = decodeText(tmp.replace(RE_ENCODED, encodedReplacer),
+              'binary',
+              charset)
+          }
+          charset = ''
+        } else if (tmp.length) {
+          tmp = decodeText(tmp, 'binary', 'utf8')
+        }
+        if (res[p] === undefined) { res[p] = tmp } else { res[p][1] = tmp }
+        tmp = ''
+        ++p
+        continue
+      } else if (!inquote && (char === ' ' || char === '\t')) { continue }
+    }
+    tmp += char
+  }
+  if (charset && tmp.length) {
+    tmp = decodeText(tmp.replace(RE_ENCODED, encodedReplacer),
+      'binary',
+      charset)
+  } else if (tmp) {
+    tmp = decodeText(tmp, 'binary', 'utf8')
+  }
+
+  if (res[p] === undefined) {
+    if (tmp) { res[p] = tmp }
+  } else { res[p][1] = tmp }
+
+  return res
+}
+
+module.exports = parseParams
diff --git a/node_modules/@fastify/busboy/package.json b/node_modules/@fastify/busboy/package.json
new file mode 100644
index 0000000..83693ac
--- /dev/null
+++ b/node_modules/@fastify/busboy/package.json
@@ -0,0 +1,86 @@
+{
+  "name": "@fastify/busboy",
+  "version": "2.1.1",
+  "private": false,
+  "author": "Brian White <mscdex@mscdex.net>",
+  "contributors": [
+    {
+      "name": "Igor Savin",
+      "email": "kibertoad@gmail.com",
+      "url": "https://github.com/kibertoad"
+    },
+    {
+      "name": "Aras Abbasi",
+      "email": "aras.abbasi@gmail.com",
+      "url": "https://github.com/uzlopak"
+    }
+  ],
+  "description": "A streaming parser for HTML form data for node.js",
+  "main": "lib/main",
+  "type": "commonjs",
+  "types": "lib/main.d.ts",
+  "scripts": {
+    "bench:busboy": "cd benchmarks && npm install && npm run benchmark-fastify",
+    "bench:dicer": "node bench/dicer/dicer-bench-multipart-parser.js",
+    "coveralls": "nyc report --reporter=lcov",
+    "lint": "npm run lint:standard",
+    "lint:everything": "npm run lint && npm run test:types",
+    "lint:fix": "standard --fix",
+    "lint:standard": "standard --verbose | snazzy",
+    "test:mocha": "tap",
+    "test:types": "tsd",
+    "test:coverage": "nyc npm run test",
+    "test": "npm run test:mocha"
+  },
+  "engines": {
+    "node": ">=14"
+  },
+  "devDependencies": {
+    "@types/node": "^20.1.0",
+    "busboy": "^1.0.0",
+    "photofinish": "^1.8.0",
+    "snazzy": "^9.0.0",
+    "standard": "^17.0.0",
+    "tap": "^16.3.8",
+    "tinybench": "^2.5.1",
+    "tsd": "^0.30.0",
+    "typescript": "^5.0.2"
+  },
+  "keywords": [
+    "uploads",
+    "forms",
+    "multipart",
+    "form-data"
+  ],
+  "license": "MIT",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/fastify/busboy.git"
+  },
+  "tsd": {
+    "directory": "test/types",
+    "compilerOptions": {
+      "esModuleInterop": false,
+      "module": "commonjs",
+      "target": "ES2017"
+    }
+  },
+  "standard": {
+    "globals": [
+      "describe",
+      "it"
+    ],
+    "ignore": [
+      "bench"
+    ]
+  },
+  "files": [
+    "README.md",
+    "LICENSE",
+    "lib/*",
+    "deps/encoding/*",
+    "deps/dicer/lib",
+    "deps/streamsearch/",
+    "deps/dicer/LICENSE"
+  ]
+}
diff --git a/node_modules/express-rate-limit/dist/index.cjs b/node_modules/express-rate-limit/dist/index.cjs
new file mode 100644
index 0000000..36d5e1b
--- /dev/null
+++ b/node_modules/express-rate-limit/dist/index.cjs
@@ -0,0 +1,838 @@
+"use strict";
+var __defProp = Object.defineProperty;
+var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
+var __getOwnPropNames = Object.getOwnPropertyNames;
+var __hasOwnProp = Object.prototype.hasOwnProperty;
+var __export = (target, all) => {
+  for (var name in all)
+    __defProp(target, name, { get: all[name], enumerable: true });
+};
+var __copyProps = (to, from, except, desc) => {
+  if (from && typeof from === "object" || typeof from === "function") {
+    for (let key of __getOwnPropNames(from))
+      if (!__hasOwnProp.call(to, key) && key !== except)
+        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
+  }
+  return to;
+};
+var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
+
+// source/index.ts
+var source_exports = {};
+__export(source_exports, {
+  MemoryStore: () => MemoryStore,
+  default: () => lib_default,
+  rateLimit: () => lib_default
+});
+module.exports = __toCommonJS(source_exports);
+
+// source/headers.ts
+var import_node_buffer = require("buffer");
+var import_node_crypto = require("crypto");
+var SUPPORTED_DRAFT_VERSIONS = ["draft-6", "draft-7", "draft-8"];
+var getResetSeconds = (resetTime, windowMs) => {
+  let resetSeconds = void 0;
+  if (resetTime) {
+    const deltaSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1e3);
+    resetSeconds = Math.max(0, deltaSeconds);
+  } else if (windowMs) {
+    resetSeconds = Math.ceil(windowMs / 1e3);
+  }
+  return resetSeconds;
+};
+var getPartitionKey = (key) => {
+  const hash = (0, import_node_crypto.createHash)("sha256");
+  hash.update(key);
+  const partitionKey = hash.digest("hex").slice(0, 12);
+  return import_node_buffer.Buffer.from(partitionKey).toString("base64");
+};
+var setLegacyHeaders = (response, info) => {
+  if (response.headersSent)
+    return;
+  response.setHeader("X-RateLimit-Limit", info.limit.toString());
+  response.setHeader("X-RateLimit-Remaining", info.remaining.toString());
+  if (info.resetTime instanceof Date) {
+    response.setHeader("Date", (/* @__PURE__ */ new Date()).toUTCString());
+    response.setHeader(
+      "X-RateLimit-Reset",
+      Math.ceil(info.resetTime.getTime() / 1e3).toString()
+    );
+  }
+};
+var setDraft6Headers = (response, info, windowMs) => {
+  if (response.headersSent)
+    return;
+  const windowSeconds = Math.ceil(windowMs / 1e3);
+  const resetSeconds = getResetSeconds(info.resetTime);
+  response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
+  response.setHeader("RateLimit-Limit", info.limit.toString());
+  response.setHeader("RateLimit-Remaining", info.remaining.toString());
+  if (resetSeconds)
+    response.setHeader("RateLimit-Reset", resetSeconds.toString());
+};
+var setDraft7Headers = (response, info, windowMs) => {
+  if (response.headersSent)
+    return;
+  const windowSeconds = Math.ceil(windowMs / 1e3);
+  const resetSeconds = getResetSeconds(info.resetTime, windowMs);
+  response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
+  response.setHeader(
+    "RateLimit",
+    `limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}`
+  );
+};
+var setDraft8Headers = (response, info, windowMs, name, key) => {
+  if (response.headersSent)
+    return;
+  const windowSeconds = Math.ceil(windowMs / 1e3);
+  const resetSeconds = getResetSeconds(info.resetTime, windowMs);
+  const partitionKey = getPartitionKey(key);
+  const policy = `q=${info.limit}; w=${windowSeconds}; pk=:${partitionKey}:`;
+  const header = `r=${info.remaining}; t=${resetSeconds}`;
+  response.append("RateLimit-Policy", `"${name}"; ${policy}`);
+  response.append("RateLimit", `"${name}"; ${header}`);
+};
+var setRetryAfterHeader = (response, info, windowMs) => {
+  if (response.headersSent)
+    return;
+  const resetSeconds = getResetSeconds(info.resetTime, windowMs);
+  response.setHeader("Retry-After", resetSeconds.toString());
+};
+
+// source/validations.ts
+var import_node_net = require("net");
+var ValidationError = class extends Error {
+  /**
+   * The code must be a string, in snake case and all capital, that starts with
+   * the substring `ERR_ERL_`.
+   *
+   * The message must be a string, starting with an uppercase character,
+   * describing the issue in detail.
+   */
+  constructor(code, message) {
+    const url = `https://express-rate-limit.github.io/${code}/`;
+    super(`${message} See ${url} for more information.`);
+    this.name = this.constructor.name;
+    this.code = code;
+    this.help = url;
+  }
+};
+var ChangeWarning = class extends ValidationError {
+};
+var usedStores = /* @__PURE__ */ new Set();
+var singleCountKeys = /* @__PURE__ */ new WeakMap();
+var validations = {
+  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
+  enabled: {
+    default: true
+  },
+  // Should be EnabledValidations type, but that's a circular reference
+  disable() {
+    for (const k of Object.keys(this.enabled))
+      this.enabled[k] = false;
+  },
+  /**
+   * Checks whether the IP address is valid, and that it does not have a port
+   * number in it.
+   *
+   * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_invalid_ip_address.
+   *
+   * @param ip {string | undefined} - The IP address provided by Express as request.ip.
+   *
+   * @returns {void}
+   */
+  ip(ip) {
+    if (ip === void 0) {
+      throw new ValidationError(
+        "ERR_ERL_UNDEFINED_IP_ADDRESS",
+        `An undefined 'request.ip' was detected. This might indicate a misconfiguration or the connection being destroyed prematurely.`
+      );
+    }
+    if (!(0, import_node_net.isIP)(ip)) {
+      throw new ValidationError(
+        "ERR_ERL_INVALID_IP_ADDRESS",
+        `An invalid 'request.ip' (${ip}) was detected. Consider passing a custom 'keyGenerator' function to the rate limiter.`
+      );
+    }
+  },
+  /**
+   * Makes sure the trust proxy setting is not set to `true`.
+   *
+   * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_permissive_trust_proxy.
+   *
+   * @param request {Request} - The Express request object.
+   *
+   * @returns {void}
+   */
+  trustProxy(request) {
+    if (request.app.get("trust proxy") === true) {
+      throw new ValidationError(
+        "ERR_ERL_PERMISSIVE_TRUST_PROXY",
+        `The Express 'trust proxy' setting is true, which allows anyone to trivially bypass IP-based rate limiting.`
+      );
+    }
+  },
+  /**
+   * Makes sure the trust proxy setting is set in case the `X-Forwarded-For`
+   * header is present.
+   *
+   * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_unset_trust_proxy.
+   *
+   * @param request {Request} - The Express request object.
+   *
+   * @returns {void}
+   */
+  xForwardedForHeader(request) {
+    if (request.headers["x-forwarded-for"] && request.app.get("trust proxy") === false) {
+      throw new ValidationError(
+        "ERR_ERL_UNEXPECTED_X_FORWARDED_FOR",
+        `The 'X-Forwarded-For' header is set but the Express 'trust proxy' setting is false (default). This could indicate a misconfiguration which would prevent express-rate-limit from accurately identifying users.`
+      );
+    }
+  },
+  /**
+   * Ensures totalHits value from store is a positive integer.
+   *
+   * @param hits {any} - The `totalHits` returned by the store.
+   */
+  positiveHits(hits) {
+    if (typeof hits !== "number" || hits < 1 || hits !== Math.round(hits)) {
+      throw new ValidationError(
+        "ERR_ERL_INVALID_HITS",
+        `The totalHits value returned from the store must be a positive integer, got ${hits}`
+      );
+    }
+  },
+  /**
+   * Ensures a single store instance is not used with multiple express-rate-limit instances
+   */
+  unsharedStore(store) {
+    if (usedStores.has(store)) {
+      const maybeUniquePrefix = store?.localKeys ? "" : " (with a unique prefix)";
+      throw new ValidationError(
+        "ERR_ERL_STORE_REUSE",
+        `A Store instance must not be shared across multiple rate limiters. Create a new instance of ${store.constructor.name}${maybeUniquePrefix} for each limiter instead.`
+      );
+    }
+    usedStores.add(store);
+  },
+  /**
+   * Ensures a given key is incremented only once per request.
+   *
+   * @param request {Request} - The Express request object.
+   * @param store {Store} - The store class.
+   * @param key {string} - The key used to store the client's hit count.
+   *
+   * @returns {void}
+   */
+  singleCount(request, store, key) {
+    let storeKeys = singleCountKeys.get(request);
+    if (!storeKeys) {
+      storeKeys = /* @__PURE__ */ new Map();
+      singleCountKeys.set(request, storeKeys);
+    }
+    const storeKey = store.localKeys ? store : store.constructor.name;
+    let keys = storeKeys.get(storeKey);
+    if (!keys) {
+      keys = [];
+      storeKeys.set(storeKey, keys);
+    }
+    const prefixedKey = `${store.prefix ?? ""}${key}`;
+    if (keys.includes(prefixedKey)) {
+      throw new ValidationError(
+        "ERR_ERL_DOUBLE_COUNT",
+        `The hit count for ${key} was incremented more than once for a single request.`
+      );
+    }
+    keys.push(prefixedKey);
+  },
+  /**
+   * Warns the user that the behaviour for `max: 0` / `limit: 0` is
+   * changing in the next major release.
+   *
+   * @param limit {number} - The maximum number of hits per client.
+   *
+   * @returns {void}
+   */
+  limit(limit) {
+    if (limit === 0) {
+      throw new ChangeWarning(
+        "WRN_ERL_MAX_ZERO",
+        `Setting limit or max to 0 disables rate limiting in express-rate-limit v6 and older, but will cause all requests to be blocked in v7`
+      );
+    }
+  },
+  /**
+   * Warns the user that the `draft_polli_ratelimit_headers` option is deprecated
+   * and will be removed in the next major release.
+   *
+   * @param draft_polli_ratelimit_headers {any | undefined} - The now-deprecated setting that was used to enable standard headers.
+   *
+   * @returns {void}
+   */
+  draftPolliHeaders(draft_polli_ratelimit_headers) {
+    if (draft_polli_ratelimit_headers) {
+      throw new ChangeWarning(
+        "WRN_ERL_DEPRECATED_DRAFT_POLLI_HEADERS",
+        `The draft_polli_ratelimit_headers configuration option is deprecated and has been removed in express-rate-limit v7, please set standardHeaders: 'draft-6' instead.`
+      );
+    }
+  },
+  /**
+   * Warns the user that the `onLimitReached` option is deprecated and
+   * will be removed in the next major release.
+   *
+   * @param onLimitReached {any | undefined} - The maximum number of hits per client.
+   *
+   * @returns {void}
+   */
+  onLimitReached(onLimitReached) {
+    if (onLimitReached) {
+      throw new ChangeWarning(
+        "WRN_ERL_DEPRECATED_ON_LIMIT_REACHED",
+        `The onLimitReached configuration option is deprecated and has been removed in express-rate-limit v7.`
+      );
+    }
+  },
+  /**
+   * Warns the user when an invalid/unsupported version of the draft spec is passed.
+   *
+   * @param version {any | undefined} - The version passed by the user.
+   *
+   * @returns {void}
+   */
+  headersDraftVersion(version) {
+    if (typeof version !== "string" || !SUPPORTED_DRAFT_VERSIONS.includes(version)) {
+      const versionString = SUPPORTED_DRAFT_VERSIONS.join(", ");
+      throw new ValidationError(
+        "ERR_ERL_HEADERS_UNSUPPORTED_DRAFT_VERSION",
+        `standardHeaders: only the following versions of the IETF draft specification are supported: ${versionString}.`
+      );
+    }
+  },
+  /**
+   * Warns the user when the selected headers option requires a reset time but
+   * the store does not provide one.
+   *
+   * @param resetTime {Date | undefined} - The timestamp when the client's hit count will be reset.
+   *
+   * @returns {void}
+   */
+  headersResetTime(resetTime) {
+    if (!resetTime) {
+      throw new ValidationError(
+        "ERR_ERL_HEADERS_NO_RESET",
+        `standardHeaders:  'draft-7' requires a 'resetTime', but the store did not provide one. The 'windowMs' value will be used instead, which may cause clients to wait longer than necessary.`
+      );
+    }
+  },
+  /**
+   * Checks the options.validate setting to ensure that only recognized
+   * validations are enabled or disabled.
+   *
+   * If any unrecognized values are found, an error is logged that
+   * includes the list of supported vaidations.
+   */
+  validationsConfig() {
+    const supportedValidations = Object.keys(this).filter(
+      (k) => !["enabled", "disable"].includes(k)
+    );
+    supportedValidations.push("default");
+    for (const key of Object.keys(this.enabled)) {
+      if (!supportedValidations.includes(key)) {
+        throw new ValidationError(
+          "ERR_ERL_UNKNOWN_VALIDATION",
+          `options.validate.${key} is not recognized. Supported validate options are: ${supportedValidations.join(
+            ", "
+          )}.`
+        );
+      }
+    }
+  },
+  /**
+   * Checks to see if the instance was created inside of a request handler,
+   * which would prevent it from working correctly, with the default memory
+   * store (or any other store with localKeys.)
+   */
+  creationStack(store) {
+    const { stack } = new Error(
+      "express-rate-limit validation check (set options.validate.creationStack=false to disable)"
+    );
+    if (stack?.includes("Layer.handle [as handle_request]")) {
+      if (!store.localKeys) {
+        throw new ValidationError(
+          "ERR_ERL_CREATED_IN_REQUEST_HANDLER",
+          "express-rate-limit instance should *usually* be created at app initialization, not when responding to a request."
+        );
+      }
+      throw new ValidationError(
+        "ERR_ERL_CREATED_IN_REQUEST_HANDLER",
+        `express-rate-limit instance should be created at app initialization, not when responding to a request.`
+      );
+    }
+  }
+};
+var getValidations = (_enabled) => {
+  let enabled;
+  if (typeof _enabled === "boolean") {
+    enabled = {
+      default: _enabled
+    };
+  } else {
+    enabled = {
+      default: true,
+      ..._enabled
+    };
+  }
+  const wrappedValidations = {
+    enabled
+  };
+  for (const [name, validation] of Object.entries(validations)) {
+    if (typeof validation === "function")
+      wrappedValidations[name] = (...args) => {
+        if (!(enabled[name] ?? enabled.default)) {
+          return;
+        }
+        try {
+          ;
+          validation.apply(
+            wrappedValidations,
+            args
+          );
+        } catch (error) {
+          if (error instanceof ChangeWarning)
+            console.warn(error);
+          else
+            console.error(error);
+        }
+      };
+  }
+  return wrappedValidations;
+};
+
+// source/memory-store.ts
+var MemoryStore = class {
+  constructor() {
+    /**
+     * These two maps store usage (requests) and reset time by key (for example, IP
+     * addresses or API keys).
+     *
+     * They are split into two to avoid having to iterate through the entire set to
+     * determine which ones need reset. Instead, `Client`s are moved from `previous`
+     * to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients
+     * left in `previous`, i.e., those that have not made any recent requests, are
+     * known to be expired and can be deleted in bulk.
+     */
+    this.previous = /* @__PURE__ */ new Map();
+    this.current = /* @__PURE__ */ new Map();
+    /**
+     * Confirmation that the keys incremented in once instance of MemoryStore
+     * cannot affect other instances.
+     */
+    this.localKeys = true;
+  }
+  /**
+   * Method that initializes the store.
+   *
+   * @param options {Options} - The options used to setup the middleware.
+   */
+  init(options) {
+    this.windowMs = options.windowMs;
+    if (this.interval)
+      clearInterval(this.interval);
+    this.interval = setInterval(() => {
+      this.clearExpired();
+    }, this.windowMs);
+    if (this.interval.unref)
+      this.interval.unref();
+  }
+  /**
+   * Method to fetch a client's hit count and reset time.
+   *
+   * @param key {string} - The identifier for a client.
+   *
+   * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
+   *
+   * @public
+   */
+  async get(key) {
+    return this.current.get(key) ?? this.previous.get(key);
+  }
+  /**
+   * Method to increment a client's hit counter.
+   *
+   * @param key {string} - The identifier for a client.
+   *
+   * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
+   *
+   * @public
+   */
+  async increment(key) {
+    const client = this.getClient(key);
+    const now = Date.now();
+    if (client.resetTime.getTime() <= now) {
+      this.resetClient(client, now);
+    }
+    client.totalHits++;
+    return client;
+  }
+  /**
+   * Method to decrement a client's hit counter.
+   *
+   * @param key {string} - The identifier for a client.
+   *
+   * @public
+   */
+  async decrement(key) {
+    const client = this.getClient(key);
+    if (client.totalHits > 0)
+      client.totalHits--;
+  }
+  /**
+   * Method to reset a client's hit counter.
+   *
+   * @param key {string} - The identifier for a client.
+   *
+   * @public
+   */
+  async resetKey(key) {
+    this.current.delete(key);
+    this.previous.delete(key);
+  }
+  /**
+   * Method to reset everyone's hit counter.
+   *
+   * @public
+   */
+  async resetAll() {
+    this.current.clear();
+    this.previous.clear();
+  }
+  /**
+   * Method to stop the timer (if currently running) and prevent any memory
+   * leaks.
+   *
+   * @public
+   */
+  shutdown() {
+    clearInterval(this.interval);
+    void this.resetAll();
+  }
+  /**
+   * Recycles a client by setting its hit count to zero, and reset time to
+   * `windowMs` milliseconds from now.
+   *
+   * NOT to be confused with `#resetKey()`, which removes a client from both the
+   * `current` and `previous` maps.
+   *
+   * @param client {Client} - The client to recycle.
+   * @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client.
+   *
+   * @return {Client} - The modified client that was passed in, to allow for chaining.
+   */
+  resetClient(client, now = Date.now()) {
+    client.totalHits = 0;
+    client.resetTime.setTime(now + this.windowMs);
+    return client;
+  }
+  /**
+   * Retrieves or creates a client, given a key. Also ensures that the client being
+   * returned is in the `current` map.
+   *
+   * @param key {string} - The key under which the client is (or is to be) stored.
+   *
+   * @returns {Client} - The requested client.
+   */
+  getClient(key) {
+    if (this.current.has(key))
+      return this.current.get(key);
+    let client;
+    if (this.previous.has(key)) {
+      client = this.previous.get(key);
+      this.previous.delete(key);
+    } else {
+      client = { totalHits: 0, resetTime: /* @__PURE__ */ new Date() };
+      this.resetClient(client);
+    }
+    this.current.set(key, client);
+    return client;
+  }
+  /**
+   * Move current clients to previous, create a new map for current.
+   *
+   * This function is called every `windowMs`.
+   */
+  clearExpired() {
+    this.previous = this.current;
+    this.current = /* @__PURE__ */ new Map();
+  }
+};
+
+// source/lib.ts
+var isLegacyStore = (store) => (
+  // Check that `incr` exists but `increment` does not - store authors might want
+  // to keep both around for backwards compatibility.
+  typeof store.incr === "function" && typeof store.increment !== "function"
+);
+var promisifyStore = (passedStore) => {
+  if (!isLegacyStore(passedStore)) {
+    return passedStore;
+  }
+  const legacyStore = passedStore;
+  class PromisifiedStore {
+    async increment(key) {
+      return new Promise((resolve, reject) => {
+        legacyStore.incr(
+          key,
+          (error, totalHits, resetTime) => {
+            if (error)
+              reject(error);
+            resolve({ totalHits, resetTime });
+          }
+        );
+      });
+    }
+    async decrement(key) {
+      return legacyStore.decrement(key);
+    }
+    async resetKey(key) {
+      return legacyStore.resetKey(key);
+    }
+    /* istanbul ignore next */
+    async resetAll() {
+      if (typeof legacyStore.resetAll === "function")
+        return legacyStore.resetAll();
+    }
+  }
+  return new PromisifiedStore();
+};
+var getOptionsFromConfig = (config) => {
+  const { validations: validations2, ...directlyPassableEntries } = config;
+  return {
+    ...directlyPassableEntries,
+    validate: validations2.enabled
+  };
+};
+var omitUndefinedOptions = (passedOptions) => {
+  const omittedOptions = {};
+  for (const k of Object.keys(passedOptions)) {
+    const key = k;
+    if (passedOptions[key] !== void 0) {
+      omittedOptions[key] = passedOptions[key];
+    }
+  }
+  return omittedOptions;
+};
+var parseOptions = (passedOptions) => {
+  const notUndefinedOptions = omitUndefinedOptions(passedOptions);
+  const validations2 = getValidations(notUndefinedOptions?.validate ?? true);
+  validations2.validationsConfig();
+  validations2.draftPolliHeaders(
+    // @ts-expect-error see the note above.
+    notUndefinedOptions.draft_polli_ratelimit_headers
+  );
+  validations2.onLimitReached(notUndefinedOptions.onLimitReached);
+  let standardHeaders = notUndefinedOptions.standardHeaders ?? false;
+  if (standardHeaders === true)
+    standardHeaders = "draft-6";
+  const config = {
+    windowMs: 60 * 1e3,
+    limit: passedOptions.max ?? 5,
+    // `max` is deprecated, but support it anyways.
+    message: "Too many requests, please try again later.",
+    statusCode: 429,
+    legacyHeaders: passedOptions.headers ?? true,
+    identifier(request, _response) {
+      let duration = "";
+      const property = config.requestPropertyName;
+      const { limit } = request[property];
+      const seconds = config.windowMs / 1e3;
+      const minutes = config.windowMs / (1e3 * 60);
+      const hours = config.windowMs / (1e3 * 60 * 60);
+      const days = config.windowMs / (1e3 * 60 * 60 * 24);
+      if (seconds < 60)
+        duration = `${seconds}sec`;
+      else if (minutes < 60)
+        duration = `${minutes}min`;
+      else if (hours < 24)
+        duration = `${hours}hr${hours > 1 ? "s" : ""}`;
+      else
+        duration = `${days}day${days > 1 ? "s" : ""}`;
+      return `${limit}-in-${duration}`;
+    },
+    requestPropertyName: "rateLimit",
+    skipFailedRequests: false,
+    skipSuccessfulRequests: false,
+    requestWasSuccessful: (_request, response) => response.statusCode < 400,
+    skip: (_request, _response) => false,
+    keyGenerator(request, _response) {
+      validations2.ip(request.ip);
+      validations2.trustProxy(request);
+      validations2.xForwardedForHeader(request);
+      return request.ip;
+    },
+    async handler(request, response, _next, _optionsUsed) {
+      response.status(config.statusCode);
+      const message = typeof config.message === "function" ? await config.message(
+        request,
+        response
+      ) : config.message;
+      if (!response.writableEnded) {
+        response.send(message);
+      }
+    },
+    passOnStoreError: false,
+    // Allow the default options to be overriden by the passed options.
+    ...notUndefinedOptions,
+    // `standardHeaders` is resolved into a draft version above, use that.
+    standardHeaders,
+    // Note that this field is declared after the user's options are spread in,
+    // so that this field doesn't get overriden with an un-promisified store!
+    store: promisifyStore(notUndefinedOptions.store ?? new MemoryStore()),
+    // Print an error to the console if a few known misconfigurations are detected.
+    validations: validations2
+  };
+  if (typeof config.store.increment !== "function" || typeof config.store.decrement !== "function" || typeof config.store.resetKey !== "function" || config.store.resetAll !== void 0 && typeof config.store.resetAll !== "function" || config.store.init !== void 0 && typeof config.store.init !== "function") {
+    throw new TypeError(
+      "An invalid store was passed. Please ensure that the store is a class that implements the `Store` interface."
+    );
+  }
+  return config;
+};
+var handleAsyncErrors = (fn) => async (request, response, next) => {
+  try {
+    await Promise.resolve(fn(request, response, next)).catch(next);
+  } catch (error) {
+    next(error);
+  }
+};
+var rateLimit = (passedOptions) => {
+  const config = parseOptions(passedOptions ?? {});
+  const options = getOptionsFromConfig(config);
+  config.validations.creationStack(config.store);
+  config.validations.unsharedStore(config.store);
+  if (typeof config.store.init === "function")
+    config.store.init(options);
+  const middleware = handleAsyncErrors(
+    async (request, response, next) => {
+      const skip = await config.skip(request, response);
+      if (skip) {
+        next();
+        return;
+      }
+      const augmentedRequest = request;
+      const key = await config.keyGenerator(request, response);
+      let totalHits = 0;
+      let resetTime;
+      try {
+        const incrementResult = await config.store.increment(key);
+        totalHits = incrementResult.totalHits;
+        resetTime = incrementResult.resetTime;
+      } catch (error) {
+        if (config.passOnStoreError) {
+          console.error(
+            "express-rate-limit: error from store, allowing request without rate-limiting.",
+            error
+          );
+          next();
+          return;
+        }
+        throw error;
+      }
+      config.validations.positiveHits(totalHits);
+      config.validations.singleCount(request, config.store, key);
+      const retrieveLimit = typeof config.limit === "function" ? config.limit(request, response) : config.limit;
+      const limit = await retrieveLimit;
+      config.validations.limit(limit);
+      const info = {
+        limit,
+        used: totalHits,
+        remaining: Math.max(limit - totalHits, 0),
+        resetTime
+      };
+      Object.defineProperty(info, "current", {
+        configurable: false,
+        enumerable: false,
+        value: totalHits
+      });
+      augmentedRequest[config.requestPropertyName] = info;
+      if (config.legacyHeaders && !response.headersSent) {
+        setLegacyHeaders(response, info);
+      }
+      if (config.standardHeaders && !response.headersSent) {
+        switch (config.standardHeaders) {
+          case "draft-6": {
+            setDraft6Headers(response, info, config.windowMs);
+            break;
+          }
+          case "draft-7": {
+            config.validations.headersResetTime(info.resetTime);
+            setDraft7Headers(response, info, config.windowMs);
+            break;
+          }
+          case "draft-8": {
+            const retrieveName = typeof config.identifier === "function" ? config.identifier(request, response) : config.identifier;
+            const name = await retrieveName;
+            config.validations.headersResetTime(info.resetTime);
+            setDraft8Headers(response, info, config.windowMs, name, key);
+            break;
+          }
+          default: {
+            config.validations.headersDraftVersion(config.standardHeaders);
+            break;
+          }
+        }
+      }
+      if (config.skipFailedRequests || config.skipSuccessfulRequests) {
+        let decremented = false;
+        const decrementKey = async () => {
+          if (!decremented) {
+            await config.store.decrement(key);
+            decremented = true;
+          }
+        };
+        if (config.skipFailedRequests) {
+          response.on("finish", async () => {
+            if (!await config.requestWasSuccessful(request, response))
+              await decrementKey();
+          });
+          response.on("close", async () => {
+            if (!response.writableEnded)
+              await decrementKey();
+          });
+          response.on("error", async () => {
+            await decrementKey();
+          });
+        }
+        if (config.skipSuccessfulRequests) {
+          response.on("finish", async () => {
+            if (await config.requestWasSuccessful(request, response))
+              await decrementKey();
+          });
+        }
+      }
+      config.validations.disable();
+      if (totalHits > limit) {
+        if (config.legacyHeaders || config.standardHeaders) {
+          setRetryAfterHeader(response, info, config.windowMs);
+        }
+        config.handler(request, response, next, options);
+        return;
+      }
+      next();
+    }
+  );
+  const getThrowFn = () => {
+    throw new Error("The current store does not support the get/getKey method");
+  };
+  middleware.resetKey = config.store.resetKey.bind(config.store);
+  middleware.getKey = typeof config.store.get === "function" ? config.store.get.bind(config.store) : getThrowFn;
+  return middleware;
+};
+var lib_default = rateLimit;
+// Annotate the CommonJS export names for ESM import in node:
+0 && (module.exports = {
+  MemoryStore,
+  rateLimit
+});
+module.exports = rateLimit; module.exports.default = rateLimit; module.exports.rateLimit = rateLimit; module.exports.MemoryStore = MemoryStore;
diff --git a/node_modules/express-rate-limit/dist/index.d.cts b/node_modules/express-rate-limit/dist/index.d.cts
new file mode 100644
index 0000000..62fe123
--- /dev/null
+++ b/node_modules/express-rate-limit/dist/index.d.cts
@@ -0,0 +1,584 @@
+// Generated by dts-bundle-generator v8.0.1
+
+import { NextFunction, Request, RequestHandler, Response } from 'express';
+
+declare const validations: {
+	enabled: {
+		[key: string]: boolean;
+	};
+	disable(): void;
+	/**
+	 * Checks whether the IP address is valid, and that it does not have a port
+	 * number in it.
+	 *
+	 * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_invalid_ip_address.
+	 *
+	 * @param ip {string | undefined} - The IP address provided by Express as request.ip.
+	 *
+	 * @returns {void}
+	 */
+	ip(ip: string | undefined): void;
+	/**
+	 * Makes sure the trust proxy setting is not set to `true`.
+	 *
+	 * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_permissive_trust_proxy.
+	 *
+	 * @param request {Request} - The Express request object.
+	 *
+	 * @returns {void}
+	 */
+	trustProxy(request: Request): void;
+	/**
+	 * Makes sure the trust proxy setting is set in case the `X-Forwarded-For`
+	 * header is present.
+	 *
+	 * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_unset_trust_proxy.
+	 *
+	 * @param request {Request} - The Express request object.
+	 *
+	 * @returns {void}
+	 */
+	xForwardedForHeader(request: Request): void;
+	/**
+	 * Ensures totalHits value from store is a positive integer.
+	 *
+	 * @param hits {any} - The `totalHits` returned by the store.
+	 */
+	positiveHits(hits: any): void;
+	/**
+	 * Ensures a single store instance is not used with multiple express-rate-limit instances
+	 */
+	unsharedStore(store: Store): void;
+	/**
+	 * Ensures a given key is incremented only once per request.
+	 *
+	 * @param request {Request} - The Express request object.
+	 * @param store {Store} - The store class.
+	 * @param key {string} - The key used to store the client's hit count.
+	 *
+	 * @returns {void}
+	 */
+	singleCount(request: Request, store: Store, key: string): void;
+	/**
+	 * Warns the user that the behaviour for `max: 0` / `limit: 0` is
+	 * changing in the next major release.
+	 *
+	 * @param limit {number} - The maximum number of hits per client.
+	 *
+	 * @returns {void}
+	 */
+	limit(limit: number): void;
+	/**
+	 * Warns the user that the `draft_polli_ratelimit_headers` option is deprecated
+	 * and will be removed in the next major release.
+	 *
+	 * @param draft_polli_ratelimit_headers {any | undefined} - The now-deprecated setting that was used to enable standard headers.
+	 *
+	 * @returns {void}
+	 */
+	draftPolliHeaders(draft_polli_ratelimit_headers?: any): void;
+	/**
+	 * Warns the user that the `onLimitReached` option is deprecated and
+	 * will be removed in the next major release.
+	 *
+	 * @param onLimitReached {any | undefined} - The maximum number of hits per client.
+	 *
+	 * @returns {void}
+	 */
+	onLimitReached(onLimitReached?: any): void;
+	/**
+	 * Warns the user when an invalid/unsupported version of the draft spec is passed.
+	 *
+	 * @param version {any | undefined} - The version passed by the user.
+	 *
+	 * @returns {void}
+	 */
+	headersDraftVersion(version?: any): void;
+	/**
+	 * Warns the user when the selected headers option requires a reset time but
+	 * the store does not provide one.
+	 *
+	 * @param resetTime {Date | undefined} - The timestamp when the client's hit count will be reset.
+	 *
+	 * @returns {void}
+	 */
+	headersResetTime(resetTime?: Date): void;
+	/**
+	 * Checks the options.validate setting to ensure that only recognized
+	 * validations are enabled or disabled.
+	 *
+	 * If any unrecognized values are found, an error is logged that
+	 * includes the list of supported vaidations.
+	 */
+	validationsConfig(): void;
+	/**
+	 * Checks to see if the instance was created inside of a request handler,
+	 * which would prevent it from working correctly, with the default memory
+	 * store (or any other store with localKeys.)
+	 */
+	creationStack(store: Store): void;
+};
+export type Validations = typeof validations;
+declare const SUPPORTED_DRAFT_VERSIONS: string[];
+/**
+ * Callback that fires when a client's hit counter is incremented.
+ *
+ * @param error {Error | undefined} - The error that occurred, if any.
+ * @param totalHits {number} - The number of hits for that client so far.
+ * @param resetTime {Date | undefined} - The time when the counter resets.
+ */
+export type IncrementCallback = (error: Error | undefined, totalHits: number, resetTime: Date | undefined) => void;
+/**
+ * Method (in the form of middleware) to generate/retrieve a value based on the
+ * incoming request.
+ *
+ * @param request {Request} - The Express request object.
+ * @param response {Response} - The Express response object.
+ *
+ * @returns {T} - The value needed.
+ */
+export type ValueDeterminingMiddleware<T> = (request: Request, response: Response) => T | Promise<T>;
+/**
+ * Express request handler that sends back a response when a client is
+ * rate-limited.
+ *
+ * @param request {Request} - The Express request object.
+ * @param response {Response} - The Express response object.
+ * @param next {NextFunction} - The Express `next` function, can be called to skip responding.
+ * @param optionsUsed {Options} - The options used to set up the middleware.
+ */
+export type RateLimitExceededEventHandler = (request: Request, response: Response, next: NextFunction, optionsUsed: Options) => void;
+/**
+ * Event callback that is triggered on a client's first request that exceeds the limit
+ * but not for subsequent requests. May be used for logging, etc. Should *not*
+ * send a response.
+ *
+ * @param request {Request} - The Express request object.
+ * @param response {Response} - The Express response object.
+ * @param optionsUsed {Options} - The options used to set up the middleware.
+ */
+export type RateLimitReachedEventHandler = (request: Request, response: Response, optionsUsed: Options) => void;
+/**
+ * Data returned from the `Store` when a client's hit counter is incremented.
+ *
+ * @property totalHits {number} - The number of hits for that client so far.
+ * @property resetTime {Date | undefined} - The time when the counter resets.
+ */
+export type ClientRateLimitInfo = {
+	totalHits: number;
+	resetTime: Date | undefined;
+};
+export type IncrementResponse = ClientRateLimitInfo;
+/**
+ * A modified Express request handler with the rate limit functions.
+ */
+export type RateLimitRequestHandler = RequestHandler & {
+	/**
+	 * Method to reset a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 */
+	resetKey: (key: string) => void;
+	/**
+	 * Method to fetch a client's hit count and reset time.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 *
+	 * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
+	 */
+	getKey: (key: string) => Promise<ClientRateLimitInfo | undefined> | ClientRateLimitInfo | undefined;
+};
+/**
+ * An interface that all hit counter stores must implement.
+ *
+ * @deprecated 6.x - Implement the `Store` interface instead.
+ */
+export type LegacyStore = {
+	/**
+	 * Method to increment a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 * @param callback {IncrementCallback} - The callback to call once the counter is incremented.
+	 */
+	incr: (key: string, callback: IncrementCallback) => void;
+	/**
+	 * Method to decrement a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 */
+	decrement: (key: string) => void;
+	/**
+	 * Method to reset a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 */
+	resetKey: (key: string) => void;
+	/**
+	 * Method to reset everyone's hit counter.
+	 */
+	resetAll?: () => void;
+};
+/**
+ * An interface that all hit counter stores must implement.
+ */
+export type Store = {
+	/**
+	 * Method that initializes the store, and has access to the options passed to
+	 * the middleware too.
+	 *
+	 * @param options {Options} - The options used to setup the middleware.
+	 */
+	init?: (options: Options) => void;
+	/**
+	 * Method to fetch a client's hit count and reset time.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 *
+	 * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
+	 */
+	get?: (key: string) => Promise<ClientRateLimitInfo | undefined> | ClientRateLimitInfo | undefined;
+	/**
+	 * Method to increment a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 *
+	 * @returns {IncrementResponse | undefined} - The number of hits and reset time for that client.
+	 */
+	increment: (key: string) => Promise<IncrementResponse> | IncrementResponse;
+	/**
+	 * Method to decrement a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 */
+	decrement: (key: string) => Promise<void> | void;
+	/**
+	 * Method to reset a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 */
+	resetKey: (key: string) => Promise<void> | void;
+	/**
+	 * Method to reset everyone's hit counter.
+	 */
+	resetAll?: () => Promise<void> | void;
+	/**
+	 * Method to shutdown the store, stop timers, and release all resources.
+	 */
+	shutdown?: () => Promise<void> | void;
+	/**
+	 * Flag to indicate that keys incremented in one instance of this store can
+	 * not affect other instances. Typically false if a database is used, true for
+	 * MemoryStore.
+	 *
+	 * Used to help detect double-counting misconfigurations.
+	 */
+	localKeys?: boolean;
+	/**
+	 * Optional value that the store prepends to keys
+	 *
+	 * Used by the double-count check to avoid false-positives when a key is counted twice, but with different prefixes
+	 */
+	prefix?: string;
+};
+export type DraftHeadersVersion = (typeof SUPPORTED_DRAFT_VERSIONS)[number];
+/**
+ * Validate configuration object for enabling or disabling specific validations.
+ *
+ * The keys must also be keys in the validations object, except `enable`, `disable`,
+ * and `default`.
+ */
+export type EnabledValidations = {
+	[key in keyof Omit<Validations, "enabled" | "disable"> | "default"]?: boolean;
+};
+/**
+ * The configuration options for the rate limiter.
+ */
+export type Options = {
+	/**
+	 * How long we should remember the requests.
+	 *
+	 * Defaults to `60000` ms (= 1 minute).
+	 */
+	windowMs: number;
+	/**
+	 * The maximum number of connections to allow during the `window` before
+	 * rate limiting the client.
+	 *
+	 * Can be the limit itself as a number or express middleware that parses
+	 * the request and then figures out the limit.
+	 *
+	 * Defaults to `5`.
+	 */
+	limit: number | ValueDeterminingMiddleware<number>;
+	/**
+	 * The response body to send back when a client is rate limited.
+	 *
+	 * Defaults to `'Too many requests, please try again later.'`
+	 */
+	message: any | ValueDeterminingMiddleware<any>;
+	/**
+	 * The HTTP status code to send back when a client is rate limited.
+	 *
+	 * Defaults to `HTTP 429 Too Many Requests` (RFC 6585).
+	 */
+	statusCode: number;
+	/**
+	 * Whether to send `X-RateLimit-*` headers with the rate limit and the number
+	 * of requests.
+	 *
+	 * Defaults to `true` (for backward compatibility).
+	 */
+	legacyHeaders: boolean;
+	/**
+	 * Whether to enable support for the standardized rate limit headers (`RateLimit-*`).
+	 *
+	 * Defaults to `false` (for backward compatibility, but its use is recommended).
+	 */
+	standardHeaders: boolean | DraftHeadersVersion;
+	/**
+	 * The name used to identify the quota policy in the `RateLimit` headers as per
+	 * the 8th draft of the IETF specification.
+	 *
+	 * Defaults to `{limit}-in-{window}`.
+	 */
+	identifier: string | ValueDeterminingMiddleware<string>;
+	/**
+	 * The name of the property on the request object to store the rate limit info.
+	 *
+	 * Defaults to `rateLimit`.
+	 */
+	requestPropertyName: string;
+	/**
+	 * If `true`, the library will (by default) skip all requests that have a 4XX
+	 * or 5XX status.
+	 *
+	 * Defaults to `false`.
+	 */
+	skipFailedRequests: boolean;
+	/**
+	 * If `true`, the library will (by default) skip all requests that have a
+	 * status code less than 400.
+	 *
+	 * Defaults to `false`.
+	 */
+	skipSuccessfulRequests: boolean;
+	/**
+	 * Method to generate custom identifiers for clients.
+	 *
+	 * By default, the client's IP address is used.
+	 */
+	keyGenerator: ValueDeterminingMiddleware<string>;
+	/**
+	 * Express request handler that sends back a response when a client is
+	 * rate-limited.
+	 *
+	 * By default, sends back the `statusCode` and `message` set via the options.
+	 */
+	handler: RateLimitExceededEventHandler;
+	/**
+	 * Method (in the form of middleware) to determine whether or not this request
+	 * counts towards a client's quota.
+	 *
+	 * By default, skips no requests.
+	 */
+	skip: ValueDeterminingMiddleware<boolean>;
+	/**
+	 * Method to determine whether or not the request counts as 'succesful'. Used
+	 * when either `skipSuccessfulRequests` or `skipFailedRequests` is set to true.
+	 *
+	 * By default, requests with a response status code less than 400 are considered
+	 * successful.
+	 */
+	requestWasSuccessful: ValueDeterminingMiddleware<boolean>;
+	/**
+	 * The `Store` to use to store the hit count for each client.
+	 *
+	 * By default, the built-in `MemoryStore` will be used.
+	 */
+	store: Store | LegacyStore;
+	/**
+	 * The list of validation checks that should run.
+	 */
+	validate: boolean | EnabledValidations;
+	/**
+	 * Whether to send `X-RateLimit-*` headers with the rate limit and the number
+	 * of requests.
+	 *
+	 * @deprecated 6.x - This option was renamed to `legacyHeaders`.
+	 */
+	headers?: boolean;
+	/**
+	 * The maximum number of connections to allow during the `window` before
+	 * rate limiting the client.
+	 *
+	 * Can be the limit itself as a number or express middleware that parses
+	 * the request and then figures out the limit.
+	 *
+	 * @deprecated 7.x - This option was renamed to `limit`. However, it will not
+	 * be removed from the library in the foreseeable future.
+	 */
+	max?: number | ValueDeterminingMiddleware<number>;
+	/**
+	 * If the Store generates an error, allow the request to pass.
+	 */
+	passOnStoreError: boolean;
+};
+/**
+ * The extended request object that includes information about the client's
+ * rate limit.
+ */
+export type AugmentedRequest = Request & {
+	[key: string]: RateLimitInfo;
+};
+/**
+ * The rate limit related information for each client included in the
+ * Express request object.
+ */
+export type RateLimitInfo = {
+	limit: number;
+	used: number;
+	remaining: number;
+	resetTime: Date | undefined;
+};
+/**
+ *
+ * Create an instance of IP rate-limiting middleware for Express.
+ *
+ * @param passedOptions {Options} - Options to configure the rate limiter.
+ *
+ * @returns {RateLimitRequestHandler} - The middleware that rate-limits clients based on your configuration.
+ *
+ * @public
+ */
+export declare const rateLimit: (passedOptions?: Partial<Options>) => RateLimitRequestHandler;
+/**
+ * The record that stores information about a client - namely, how many times
+ * they have hit the endpoint, and when their hit count resets.
+ *
+ * Similar to `ClientRateLimitInfo`, except `resetTime` is a compulsory field.
+ */
+export type Client = {
+	totalHits: number;
+	resetTime: Date;
+};
+/**
+ * A `Store` that stores the hit count for each client in memory.
+ *
+ * @public
+ */
+export declare class MemoryStore implements Store {
+	/**
+	 * The duration of time before which all hit counts are reset (in milliseconds).
+	 */
+	windowMs: number;
+	/**
+	 * These two maps store usage (requests) and reset time by key (for example, IP
+	 * addresses or API keys).
+	 *
+	 * They are split into two to avoid having to iterate through the entire set to
+	 * determine which ones need reset. Instead, `Client`s are moved from `previous`
+	 * to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients
+	 * left in `previous`, i.e., those that have not made any recent requests, are
+	 * known to be expired and can be deleted in bulk.
+	 */
+	previous: Map<string, Client>;
+	current: Map<string, Client>;
+	/**
+	 * A reference to the active timer.
+	 */
+	interval?: NodeJS.Timeout;
+	/**
+	 * Confirmation that the keys incremented in once instance of MemoryStore
+	 * cannot affect other instances.
+	 */
+	localKeys: boolean;
+	/**
+	 * Method that initializes the store.
+	 *
+	 * @param options {Options} - The options used to setup the middleware.
+	 */
+	init(options: Options): void;
+	/**
+	 * Method to fetch a client's hit count and reset time.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 *
+	 * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
+	 *
+	 * @public
+	 */
+	get(key: string): Promise<ClientRateLimitInfo | undefined>;
+	/**
+	 * Method to increment a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 *
+	 * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
+	 *
+	 * @public
+	 */
+	increment(key: string): Promise<ClientRateLimitInfo>;
+	/**
+	 * Method to decrement a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 *
+	 * @public
+	 */
+	decrement(key: string): Promise<void>;
+	/**
+	 * Method to reset a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 *
+	 * @public
+	 */
+	resetKey(key: string): Promise<void>;
+	/**
+	 * Method to reset everyone's hit counter.
+	 *
+	 * @public
+	 */
+	resetAll(): Promise<void>;
+	/**
+	 * Method to stop the timer (if currently running) and prevent any memory
+	 * leaks.
+	 *
+	 * @public
+	 */
+	shutdown(): void;
+	/**
+	 * Recycles a client by setting its hit count to zero, and reset time to
+	 * `windowMs` milliseconds from now.
+	 *
+	 * NOT to be confused with `#resetKey()`, which removes a client from both the
+	 * `current` and `previous` maps.
+	 *
+	 * @param client {Client} - The client to recycle.
+	 * @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client.
+	 *
+	 * @return {Client} - The modified client that was passed in, to allow for chaining.
+	 */
+	private resetClient;
+	/**
+	 * Retrieves or creates a client, given a key. Also ensures that the client being
+	 * returned is in the `current` map.
+	 *
+	 * @param key {string} - The key under which the client is (or is to be) stored.
+	 *
+	 * @returns {Client} - The requested client.
+	 */
+	private getClient;
+	/**
+	 * Move current clients to previous, create a new map for current.
+	 *
+	 * This function is called every `windowMs`.
+	 */
+	private clearExpired;
+}
+
+export {
+	rateLimit as default,
+};
+
+export {};
diff --git a/node_modules/express-rate-limit/dist/index.d.mts b/node_modules/express-rate-limit/dist/index.d.mts
new file mode 100644
index 0000000..62fe123
--- /dev/null
+++ b/node_modules/express-rate-limit/dist/index.d.mts
@@ -0,0 +1,584 @@
+// Generated by dts-bundle-generator v8.0.1
+
+import { NextFunction, Request, RequestHandler, Response } from 'express';
+
+declare const validations: {
+	enabled: {
+		[key: string]: boolean;
+	};
+	disable(): void;
+	/**
+	 * Checks whether the IP address is valid, and that it does not have a port
+	 * number in it.
+	 *
+	 * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_invalid_ip_address.
+	 *
+	 * @param ip {string | undefined} - The IP address provided by Express as request.ip.
+	 *
+	 * @returns {void}
+	 */
+	ip(ip: string | undefined): void;
+	/**
+	 * Makes sure the trust proxy setting is not set to `true`.
+	 *
+	 * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_permissive_trust_proxy.
+	 *
+	 * @param request {Request} - The Express request object.
+	 *
+	 * @returns {void}
+	 */
+	trustProxy(request: Request): void;
+	/**
+	 * Makes sure the trust proxy setting is set in case the `X-Forwarded-For`
+	 * header is present.
+	 *
+	 * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_unset_trust_proxy.
+	 *
+	 * @param request {Request} - The Express request object.
+	 *
+	 * @returns {void}
+	 */
+	xForwardedForHeader(request: Request): void;
+	/**
+	 * Ensures totalHits value from store is a positive integer.
+	 *
+	 * @param hits {any} - The `totalHits` returned by the store.
+	 */
+	positiveHits(hits: any): void;
+	/**
+	 * Ensures a single store instance is not used with multiple express-rate-limit instances
+	 */
+	unsharedStore(store: Store): void;
+	/**
+	 * Ensures a given key is incremented only once per request.
+	 *
+	 * @param request {Request} - The Express request object.
+	 * @param store {Store} - The store class.
+	 * @param key {string} - The key used to store the client's hit count.
+	 *
+	 * @returns {void}
+	 */
+	singleCount(request: Request, store: Store, key: string): void;
+	/**
+	 * Warns the user that the behaviour for `max: 0` / `limit: 0` is
+	 * changing in the next major release.
+	 *
+	 * @param limit {number} - The maximum number of hits per client.
+	 *
+	 * @returns {void}
+	 */
+	limit(limit: number): void;
+	/**
+	 * Warns the user that the `draft_polli_ratelimit_headers` option is deprecated
+	 * and will be removed in the next major release.
+	 *
+	 * @param draft_polli_ratelimit_headers {any | undefined} - The now-deprecated setting that was used to enable standard headers.
+	 *
+	 * @returns {void}
+	 */
+	draftPolliHeaders(draft_polli_ratelimit_headers?: any): void;
+	/**
+	 * Warns the user that the `onLimitReached` option is deprecated and
+	 * will be removed in the next major release.
+	 *
+	 * @param onLimitReached {any | undefined} - The maximum number of hits per client.
+	 *
+	 * @returns {void}
+	 */
+	onLimitReached(onLimitReached?: any): void;
+	/**
+	 * Warns the user when an invalid/unsupported version of the draft spec is passed.
+	 *
+	 * @param version {any | undefined} - The version passed by the user.
+	 *
+	 * @returns {void}
+	 */
+	headersDraftVersion(version?: any): void;
+	/**
+	 * Warns the user when the selected headers option requires a reset time but
+	 * the store does not provide one.
+	 *
+	 * @param resetTime {Date | undefined} - The timestamp when the client's hit count will be reset.
+	 *
+	 * @returns {void}
+	 */
+	headersResetTime(resetTime?: Date): void;
+	/**
+	 * Checks the options.validate setting to ensure that only recognized
+	 * validations are enabled or disabled.
+	 *
+	 * If any unrecognized values are found, an error is logged that
+	 * includes the list of supported vaidations.
+	 */
+	validationsConfig(): void;
+	/**
+	 * Checks to see if the instance was created inside of a request handler,
+	 * which would prevent it from working correctly, with the default memory
+	 * store (or any other store with localKeys.)
+	 */
+	creationStack(store: Store): void;
+};
+export type Validations = typeof validations;
+declare const SUPPORTED_DRAFT_VERSIONS: string[];
+/**
+ * Callback that fires when a client's hit counter is incremented.
+ *
+ * @param error {Error | undefined} - The error that occurred, if any.
+ * @param totalHits {number} - The number of hits for that client so far.
+ * @param resetTime {Date | undefined} - The time when the counter resets.
+ */
+export type IncrementCallback = (error: Error | undefined, totalHits: number, resetTime: Date | undefined) => void;
+/**
+ * Method (in the form of middleware) to generate/retrieve a value based on the
+ * incoming request.
+ *
+ * @param request {Request} - The Express request object.
+ * @param response {Response} - The Express response object.
+ *
+ * @returns {T} - The value needed.
+ */
+export type ValueDeterminingMiddleware<T> = (request: Request, response: Response) => T | Promise<T>;
+/**
+ * Express request handler that sends back a response when a client is
+ * rate-limited.
+ *
+ * @param request {Request} - The Express request object.
+ * @param response {Response} - The Express response object.
+ * @param next {NextFunction} - The Express `next` function, can be called to skip responding.
+ * @param optionsUsed {Options} - The options used to set up the middleware.
+ */
+export type RateLimitExceededEventHandler = (request: Request, response: Response, next: NextFunction, optionsUsed: Options) => void;
+/**
+ * Event callback that is triggered on a client's first request that exceeds the limit
+ * but not for subsequent requests. May be used for logging, etc. Should *not*
+ * send a response.
+ *
+ * @param request {Request} - The Express request object.
+ * @param response {Response} - The Express response object.
+ * @param optionsUsed {Options} - The options used to set up the middleware.
+ */
+export type RateLimitReachedEventHandler = (request: Request, response: Response, optionsUsed: Options) => void;
+/**
+ * Data returned from the `Store` when a client's hit counter is incremented.
+ *
+ * @property totalHits {number} - The number of hits for that client so far.
+ * @property resetTime {Date | undefined} - The time when the counter resets.
+ */
+export type ClientRateLimitInfo = {
+	totalHits: number;
+	resetTime: Date | undefined;
+};
+export type IncrementResponse = ClientRateLimitInfo;
+/**
+ * A modified Express request handler with the rate limit functions.
+ */
+export type RateLimitRequestHandler = RequestHandler & {
+	/**
+	 * Method to reset a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 */
+	resetKey: (key: string) => void;
+	/**
+	 * Method to fetch a client's hit count and reset time.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 *
+	 * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
+	 */
+	getKey: (key: string) => Promise<ClientRateLimitInfo | undefined> | ClientRateLimitInfo | undefined;
+};
+/**
+ * An interface that all hit counter stores must implement.
+ *
+ * @deprecated 6.x - Implement the `Store` interface instead.
+ */
+export type LegacyStore = {
+	/**
+	 * Method to increment a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 * @param callback {IncrementCallback} - The callback to call once the counter is incremented.
+	 */
+	incr: (key: string, callback: IncrementCallback) => void;
+	/**
+	 * Method to decrement a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 */
+	decrement: (key: string) => void;
+	/**
+	 * Method to reset a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 */
+	resetKey: (key: string) => void;
+	/**
+	 * Method to reset everyone's hit counter.
+	 */
+	resetAll?: () => void;
+};
+/**
+ * An interface that all hit counter stores must implement.
+ */
+export type Store = {
+	/**
+	 * Method that initializes the store, and has access to the options passed to
+	 * the middleware too.
+	 *
+	 * @param options {Options} - The options used to setup the middleware.
+	 */
+	init?: (options: Options) => void;
+	/**
+	 * Method to fetch a client's hit count and reset time.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 *
+	 * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
+	 */
+	get?: (key: string) => Promise<ClientRateLimitInfo | undefined> | ClientRateLimitInfo | undefined;
+	/**
+	 * Method to increment a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 *
+	 * @returns {IncrementResponse | undefined} - The number of hits and reset time for that client.
+	 */
+	increment: (key: string) => Promise<IncrementResponse> | IncrementResponse;
+	/**
+	 * Method to decrement a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 */
+	decrement: (key: string) => Promise<void> | void;
+	/**
+	 * Method to reset a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 */
+	resetKey: (key: string) => Promise<void> | void;
+	/**
+	 * Method to reset everyone's hit counter.
+	 */
+	resetAll?: () => Promise<void> | void;
+	/**
+	 * Method to shutdown the store, stop timers, and release all resources.
+	 */
+	shutdown?: () => Promise<void> | void;
+	/**
+	 * Flag to indicate that keys incremented in one instance of this store can
+	 * not affect other instances. Typically false if a database is used, true for
+	 * MemoryStore.
+	 *
+	 * Used to help detect double-counting misconfigurations.
+	 */
+	localKeys?: boolean;
+	/**
+	 * Optional value that the store prepends to keys
+	 *
+	 * Used by the double-count check to avoid false-positives when a key is counted twice, but with different prefixes
+	 */
+	prefix?: string;
+};
+export type DraftHeadersVersion = (typeof SUPPORTED_DRAFT_VERSIONS)[number];
+/**
+ * Validate configuration object for enabling or disabling specific validations.
+ *
+ * The keys must also be keys in the validations object, except `enable`, `disable`,
+ * and `default`.
+ */
+export type EnabledValidations = {
+	[key in keyof Omit<Validations, "enabled" | "disable"> | "default"]?: boolean;
+};
+/**
+ * The configuration options for the rate limiter.
+ */
+export type Options = {
+	/**
+	 * How long we should remember the requests.
+	 *
+	 * Defaults to `60000` ms (= 1 minute).
+	 */
+	windowMs: number;
+	/**
+	 * The maximum number of connections to allow during the `window` before
+	 * rate limiting the client.
+	 *
+	 * Can be the limit itself as a number or express middleware that parses
+	 * the request and then figures out the limit.
+	 *
+	 * Defaults to `5`.
+	 */
+	limit: number | ValueDeterminingMiddleware<number>;
+	/**
+	 * The response body to send back when a client is rate limited.
+	 *
+	 * Defaults to `'Too many requests, please try again later.'`
+	 */
+	message: any | ValueDeterminingMiddleware<any>;
+	/**
+	 * The HTTP status code to send back when a client is rate limited.
+	 *
+	 * Defaults to `HTTP 429 Too Many Requests` (RFC 6585).
+	 */
+	statusCode: number;
+	/**
+	 * Whether to send `X-RateLimit-*` headers with the rate limit and the number
+	 * of requests.
+	 *
+	 * Defaults to `true` (for backward compatibility).
+	 */
+	legacyHeaders: boolean;
+	/**
+	 * Whether to enable support for the standardized rate limit headers (`RateLimit-*`).
+	 *
+	 * Defaults to `false` (for backward compatibility, but its use is recommended).
+	 */
+	standardHeaders: boolean | DraftHeadersVersion;
+	/**
+	 * The name used to identify the quota policy in the `RateLimit` headers as per
+	 * the 8th draft of the IETF specification.
+	 *
+	 * Defaults to `{limit}-in-{window}`.
+	 */
+	identifier: string | ValueDeterminingMiddleware<string>;
+	/**
+	 * The name of the property on the request object to store the rate limit info.
+	 *
+	 * Defaults to `rateLimit`.
+	 */
+	requestPropertyName: string;
+	/**
+	 * If `true`, the library will (by default) skip all requests that have a 4XX
+	 * or 5XX status.
+	 *
+	 * Defaults to `false`.
+	 */
+	skipFailedRequests: boolean;
+	/**
+	 * If `true`, the library will (by default) skip all requests that have a
+	 * status code less than 400.
+	 *
+	 * Defaults to `false`.
+	 */
+	skipSuccessfulRequests: boolean;
+	/**
+	 * Method to generate custom identifiers for clients.
+	 *
+	 * By default, the client's IP address is used.
+	 */
+	keyGenerator: ValueDeterminingMiddleware<string>;
+	/**
+	 * Express request handler that sends back a response when a client is
+	 * rate-limited.
+	 *
+	 * By default, sends back the `statusCode` and `message` set via the options.
+	 */
+	handler: RateLimitExceededEventHandler;
+	/**
+	 * Method (in the form of middleware) to determine whether or not this request
+	 * counts towards a client's quota.
+	 *
+	 * By default, skips no requests.
+	 */
+	skip: ValueDeterminingMiddleware<boolean>;
+	/**
+	 * Method to determine whether or not the request counts as 'succesful'. Used
+	 * when either `skipSuccessfulRequests` or `skipFailedRequests` is set to true.
+	 *
+	 * By default, requests with a response status code less than 400 are considered
+	 * successful.
+	 */
+	requestWasSuccessful: ValueDeterminingMiddleware<boolean>;
+	/**
+	 * The `Store` to use to store the hit count for each client.
+	 *
+	 * By default, the built-in `MemoryStore` will be used.
+	 */
+	store: Store | LegacyStore;
+	/**
+	 * The list of validation checks that should run.
+	 */
+	validate: boolean | EnabledValidations;
+	/**
+	 * Whether to send `X-RateLimit-*` headers with the rate limit and the number
+	 * of requests.
+	 *
+	 * @deprecated 6.x - This option was renamed to `legacyHeaders`.
+	 */
+	headers?: boolean;
+	/**
+	 * The maximum number of connections to allow during the `window` before
+	 * rate limiting the client.
+	 *
+	 * Can be the limit itself as a number or express middleware that parses
+	 * the request and then figures out the limit.
+	 *
+	 * @deprecated 7.x - This option was renamed to `limit`. However, it will not
+	 * be removed from the library in the foreseeable future.
+	 */
+	max?: number | ValueDeterminingMiddleware<number>;
+	/**
+	 * If the Store generates an error, allow the request to pass.
+	 */
+	passOnStoreError: boolean;
+};
+/**
+ * The extended request object that includes information about the client's
+ * rate limit.
+ */
+export type AugmentedRequest = Request & {
+	[key: string]: RateLimitInfo;
+};
+/**
+ * The rate limit related information for each client included in the
+ * Express request object.
+ */
+export type RateLimitInfo = {
+	limit: number;
+	used: number;
+	remaining: number;
+	resetTime: Date | undefined;
+};
+/**
+ *
+ * Create an instance of IP rate-limiting middleware for Express.
+ *
+ * @param passedOptions {Options} - Options to configure the rate limiter.
+ *
+ * @returns {RateLimitRequestHandler} - The middleware that rate-limits clients based on your configuration.
+ *
+ * @public
+ */
+export declare const rateLimit: (passedOptions?: Partial<Options>) => RateLimitRequestHandler;
+/**
+ * The record that stores information about a client - namely, how many times
+ * they have hit the endpoint, and when their hit count resets.
+ *
+ * Similar to `ClientRateLimitInfo`, except `resetTime` is a compulsory field.
+ */
+export type Client = {
+	totalHits: number;
+	resetTime: Date;
+};
+/**
+ * A `Store` that stores the hit count for each client in memory.
+ *
+ * @public
+ */
+export declare class MemoryStore implements Store {
+	/**
+	 * The duration of time before which all hit counts are reset (in milliseconds).
+	 */
+	windowMs: number;
+	/**
+	 * These two maps store usage (requests) and reset time by key (for example, IP
+	 * addresses or API keys).
+	 *
+	 * They are split into two to avoid having to iterate through the entire set to
+	 * determine which ones need reset. Instead, `Client`s are moved from `previous`
+	 * to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients
+	 * left in `previous`, i.e., those that have not made any recent requests, are
+	 * known to be expired and can be deleted in bulk.
+	 */
+	previous: Map<string, Client>;
+	current: Map<string, Client>;
+	/**
+	 * A reference to the active timer.
+	 */
+	interval?: NodeJS.Timeout;
+	/**
+	 * Confirmation that the keys incremented in once instance of MemoryStore
+	 * cannot affect other instances.
+	 */
+	localKeys: boolean;
+	/**
+	 * Method that initializes the store.
+	 *
+	 * @param options {Options} - The options used to setup the middleware.
+	 */
+	init(options: Options): void;
+	/**
+	 * Method to fetch a client's hit count and reset time.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 *
+	 * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
+	 *
+	 * @public
+	 */
+	get(key: string): Promise<ClientRateLimitInfo | undefined>;
+	/**
+	 * Method to increment a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 *
+	 * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
+	 *
+	 * @public
+	 */
+	increment(key: string): Promise<ClientRateLimitInfo>;
+	/**
+	 * Method to decrement a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 *
+	 * @public
+	 */
+	decrement(key: string): Promise<void>;
+	/**
+	 * Method to reset a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 *
+	 * @public
+	 */
+	resetKey(key: string): Promise<void>;
+	/**
+	 * Method to reset everyone's hit counter.
+	 *
+	 * @public
+	 */
+	resetAll(): Promise<void>;
+	/**
+	 * Method to stop the timer (if currently running) and prevent any memory
+	 * leaks.
+	 *
+	 * @public
+	 */
+	shutdown(): void;
+	/**
+	 * Recycles a client by setting its hit count to zero, and reset time to
+	 * `windowMs` milliseconds from now.
+	 *
+	 * NOT to be confused with `#resetKey()`, which removes a client from both the
+	 * `current` and `previous` maps.
+	 *
+	 * @param client {Client} - The client to recycle.
+	 * @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client.
+	 *
+	 * @return {Client} - The modified client that was passed in, to allow for chaining.
+	 */
+	private resetClient;
+	/**
+	 * Retrieves or creates a client, given a key. Also ensures that the client being
+	 * returned is in the `current` map.
+	 *
+	 * @param key {string} - The key under which the client is (or is to be) stored.
+	 *
+	 * @returns {Client} - The requested client.
+	 */
+	private getClient;
+	/**
+	 * Move current clients to previous, create a new map for current.
+	 *
+	 * This function is called every `windowMs`.
+	 */
+	private clearExpired;
+}
+
+export {
+	rateLimit as default,
+};
+
+export {};
diff --git a/node_modules/express-rate-limit/dist/index.d.ts b/node_modules/express-rate-limit/dist/index.d.ts
new file mode 100644
index 0000000..62fe123
--- /dev/null
+++ b/node_modules/express-rate-limit/dist/index.d.ts
@@ -0,0 +1,584 @@
+// Generated by dts-bundle-generator v8.0.1
+
+import { NextFunction, Request, RequestHandler, Response } from 'express';
+
+declare const validations: {
+	enabled: {
+		[key: string]: boolean;
+	};
+	disable(): void;
+	/**
+	 * Checks whether the IP address is valid, and that it does not have a port
+	 * number in it.
+	 *
+	 * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_invalid_ip_address.
+	 *
+	 * @param ip {string | undefined} - The IP address provided by Express as request.ip.
+	 *
+	 * @returns {void}
+	 */
+	ip(ip: string | undefined): void;
+	/**
+	 * Makes sure the trust proxy setting is not set to `true`.
+	 *
+	 * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_permissive_trust_proxy.
+	 *
+	 * @param request {Request} - The Express request object.
+	 *
+	 * @returns {void}
+	 */
+	trustProxy(request: Request): void;
+	/**
+	 * Makes sure the trust proxy setting is set in case the `X-Forwarded-For`
+	 * header is present.
+	 *
+	 * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_unset_trust_proxy.
+	 *
+	 * @param request {Request} - The Express request object.
+	 *
+	 * @returns {void}
+	 */
+	xForwardedForHeader(request: Request): void;
+	/**
+	 * Ensures totalHits value from store is a positive integer.
+	 *
+	 * @param hits {any} - The `totalHits` returned by the store.
+	 */
+	positiveHits(hits: any): void;
+	/**
+	 * Ensures a single store instance is not used with multiple express-rate-limit instances
+	 */
+	unsharedStore(store: Store): void;
+	/**
+	 * Ensures a given key is incremented only once per request.
+	 *
+	 * @param request {Request} - The Express request object.
+	 * @param store {Store} - The store class.
+	 * @param key {string} - The key used to store the client's hit count.
+	 *
+	 * @returns {void}
+	 */
+	singleCount(request: Request, store: Store, key: string): void;
+	/**
+	 * Warns the user that the behaviour for `max: 0` / `limit: 0` is
+	 * changing in the next major release.
+	 *
+	 * @param limit {number} - The maximum number of hits per client.
+	 *
+	 * @returns {void}
+	 */
+	limit(limit: number): void;
+	/**
+	 * Warns the user that the `draft_polli_ratelimit_headers` option is deprecated
+	 * and will be removed in the next major release.
+	 *
+	 * @param draft_polli_ratelimit_headers {any | undefined} - The now-deprecated setting that was used to enable standard headers.
+	 *
+	 * @returns {void}
+	 */
+	draftPolliHeaders(draft_polli_ratelimit_headers?: any): void;
+	/**
+	 * Warns the user that the `onLimitReached` option is deprecated and
+	 * will be removed in the next major release.
+	 *
+	 * @param onLimitReached {any | undefined} - The maximum number of hits per client.
+	 *
+	 * @returns {void}
+	 */
+	onLimitReached(onLimitReached?: any): void;
+	/**
+	 * Warns the user when an invalid/unsupported version of the draft spec is passed.
+	 *
+	 * @param version {any | undefined} - The version passed by the user.
+	 *
+	 * @returns {void}
+	 */
+	headersDraftVersion(version?: any): void;
+	/**
+	 * Warns the user when the selected headers option requires a reset time but
+	 * the store does not provide one.
+	 *
+	 * @param resetTime {Date | undefined} - The timestamp when the client's hit count will be reset.
+	 *
+	 * @returns {void}
+	 */
+	headersResetTime(resetTime?: Date): void;
+	/**
+	 * Checks the options.validate setting to ensure that only recognized
+	 * validations are enabled or disabled.
+	 *
+	 * If any unrecognized values are found, an error is logged that
+	 * includes the list of supported vaidations.
+	 */
+	validationsConfig(): void;
+	/**
+	 * Checks to see if the instance was created inside of a request handler,
+	 * which would prevent it from working correctly, with the default memory
+	 * store (or any other store with localKeys.)
+	 */
+	creationStack(store: Store): void;
+};
+export type Validations = typeof validations;
+declare const SUPPORTED_DRAFT_VERSIONS: string[];
+/**
+ * Callback that fires when a client's hit counter is incremented.
+ *
+ * @param error {Error | undefined} - The error that occurred, if any.
+ * @param totalHits {number} - The number of hits for that client so far.
+ * @param resetTime {Date | undefined} - The time when the counter resets.
+ */
+export type IncrementCallback = (error: Error | undefined, totalHits: number, resetTime: Date | undefined) => void;
+/**
+ * Method (in the form of middleware) to generate/retrieve a value based on the
+ * incoming request.
+ *
+ * @param request {Request} - The Express request object.
+ * @param response {Response} - The Express response object.
+ *
+ * @returns {T} - The value needed.
+ */
+export type ValueDeterminingMiddleware<T> = (request: Request, response: Response) => T | Promise<T>;
+/**
+ * Express request handler that sends back a response when a client is
+ * rate-limited.
+ *
+ * @param request {Request} - The Express request object.
+ * @param response {Response} - The Express response object.
+ * @param next {NextFunction} - The Express `next` function, can be called to skip responding.
+ * @param optionsUsed {Options} - The options used to set up the middleware.
+ */
+export type RateLimitExceededEventHandler = (request: Request, response: Response, next: NextFunction, optionsUsed: Options) => void;
+/**
+ * Event callback that is triggered on a client's first request that exceeds the limit
+ * but not for subsequent requests. May be used for logging, etc. Should *not*
+ * send a response.
+ *
+ * @param request {Request} - The Express request object.
+ * @param response {Response} - The Express response object.
+ * @param optionsUsed {Options} - The options used to set up the middleware.
+ */
+export type RateLimitReachedEventHandler = (request: Request, response: Response, optionsUsed: Options) => void;
+/**
+ * Data returned from the `Store` when a client's hit counter is incremented.
+ *
+ * @property totalHits {number} - The number of hits for that client so far.
+ * @property resetTime {Date | undefined} - The time when the counter resets.
+ */
+export type ClientRateLimitInfo = {
+	totalHits: number;
+	resetTime: Date | undefined;
+};
+export type IncrementResponse = ClientRateLimitInfo;
+/**
+ * A modified Express request handler with the rate limit functions.
+ */
+export type RateLimitRequestHandler = RequestHandler & {
+	/**
+	 * Method to reset a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 */
+	resetKey: (key: string) => void;
+	/**
+	 * Method to fetch a client's hit count and reset time.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 *
+	 * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
+	 */
+	getKey: (key: string) => Promise<ClientRateLimitInfo | undefined> | ClientRateLimitInfo | undefined;
+};
+/**
+ * An interface that all hit counter stores must implement.
+ *
+ * @deprecated 6.x - Implement the `Store` interface instead.
+ */
+export type LegacyStore = {
+	/**
+	 * Method to increment a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 * @param callback {IncrementCallback} - The callback to call once the counter is incremented.
+	 */
+	incr: (key: string, callback: IncrementCallback) => void;
+	/**
+	 * Method to decrement a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 */
+	decrement: (key: string) => void;
+	/**
+	 * Method to reset a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 */
+	resetKey: (key: string) => void;
+	/**
+	 * Method to reset everyone's hit counter.
+	 */
+	resetAll?: () => void;
+};
+/**
+ * An interface that all hit counter stores must implement.
+ */
+export type Store = {
+	/**
+	 * Method that initializes the store, and has access to the options passed to
+	 * the middleware too.
+	 *
+	 * @param options {Options} - The options used to setup the middleware.
+	 */
+	init?: (options: Options) => void;
+	/**
+	 * Method to fetch a client's hit count and reset time.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 *
+	 * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
+	 */
+	get?: (key: string) => Promise<ClientRateLimitInfo | undefined> | ClientRateLimitInfo | undefined;
+	/**
+	 * Method to increment a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 *
+	 * @returns {IncrementResponse | undefined} - The number of hits and reset time for that client.
+	 */
+	increment: (key: string) => Promise<IncrementResponse> | IncrementResponse;
+	/**
+	 * Method to decrement a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 */
+	decrement: (key: string) => Promise<void> | void;
+	/**
+	 * Method to reset a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 */
+	resetKey: (key: string) => Promise<void> | void;
+	/**
+	 * Method to reset everyone's hit counter.
+	 */
+	resetAll?: () => Promise<void> | void;
+	/**
+	 * Method to shutdown the store, stop timers, and release all resources.
+	 */
+	shutdown?: () => Promise<void> | void;
+	/**
+	 * Flag to indicate that keys incremented in one instance of this store can
+	 * not affect other instances. Typically false if a database is used, true for
+	 * MemoryStore.
+	 *
+	 * Used to help detect double-counting misconfigurations.
+	 */
+	localKeys?: boolean;
+	/**
+	 * Optional value that the store prepends to keys
+	 *
+	 * Used by the double-count check to avoid false-positives when a key is counted twice, but with different prefixes
+	 */
+	prefix?: string;
+};
+export type DraftHeadersVersion = (typeof SUPPORTED_DRAFT_VERSIONS)[number];
+/**
+ * Validate configuration object for enabling or disabling specific validations.
+ *
+ * The keys must also be keys in the validations object, except `enable`, `disable`,
+ * and `default`.
+ */
+export type EnabledValidations = {
+	[key in keyof Omit<Validations, "enabled" | "disable"> | "default"]?: boolean;
+};
+/**
+ * The configuration options for the rate limiter.
+ */
+export type Options = {
+	/**
+	 * How long we should remember the requests.
+	 *
+	 * Defaults to `60000` ms (= 1 minute).
+	 */
+	windowMs: number;
+	/**
+	 * The maximum number of connections to allow during the `window` before
+	 * rate limiting the client.
+	 *
+	 * Can be the limit itself as a number or express middleware that parses
+	 * the request and then figures out the limit.
+	 *
+	 * Defaults to `5`.
+	 */
+	limit: number | ValueDeterminingMiddleware<number>;
+	/**
+	 * The response body to send back when a client is rate limited.
+	 *
+	 * Defaults to `'Too many requests, please try again later.'`
+	 */
+	message: any | ValueDeterminingMiddleware<any>;
+	/**
+	 * The HTTP status code to send back when a client is rate limited.
+	 *
+	 * Defaults to `HTTP 429 Too Many Requests` (RFC 6585).
+	 */
+	statusCode: number;
+	/**
+	 * Whether to send `X-RateLimit-*` headers with the rate limit and the number
+	 * of requests.
+	 *
+	 * Defaults to `true` (for backward compatibility).
+	 */
+	legacyHeaders: boolean;
+	/**
+	 * Whether to enable support for the standardized rate limit headers (`RateLimit-*`).
+	 *
+	 * Defaults to `false` (for backward compatibility, but its use is recommended).
+	 */
+	standardHeaders: boolean | DraftHeadersVersion;
+	/**
+	 * The name used to identify the quota policy in the `RateLimit` headers as per
+	 * the 8th draft of the IETF specification.
+	 *
+	 * Defaults to `{limit}-in-{window}`.
+	 */
+	identifier: string | ValueDeterminingMiddleware<string>;
+	/**
+	 * The name of the property on the request object to store the rate limit info.
+	 *
+	 * Defaults to `rateLimit`.
+	 */
+	requestPropertyName: string;
+	/**
+	 * If `true`, the library will (by default) skip all requests that have a 4XX
+	 * or 5XX status.
+	 *
+	 * Defaults to `false`.
+	 */
+	skipFailedRequests: boolean;
+	/**
+	 * If `true`, the library will (by default) skip all requests that have a
+	 * status code less than 400.
+	 *
+	 * Defaults to `false`.
+	 */
+	skipSuccessfulRequests: boolean;
+	/**
+	 * Method to generate custom identifiers for clients.
+	 *
+	 * By default, the client's IP address is used.
+	 */
+	keyGenerator: ValueDeterminingMiddleware<string>;
+	/**
+	 * Express request handler that sends back a response when a client is
+	 * rate-limited.
+	 *
+	 * By default, sends back the `statusCode` and `message` set via the options.
+	 */
+	handler: RateLimitExceededEventHandler;
+	/**
+	 * Method (in the form of middleware) to determine whether or not this request
+	 * counts towards a client's quota.
+	 *
+	 * By default, skips no requests.
+	 */
+	skip: ValueDeterminingMiddleware<boolean>;
+	/**
+	 * Method to determine whether or not the request counts as 'succesful'. Used
+	 * when either `skipSuccessfulRequests` or `skipFailedRequests` is set to true.
+	 *
+	 * By default, requests with a response status code less than 400 are considered
+	 * successful.
+	 */
+	requestWasSuccessful: ValueDeterminingMiddleware<boolean>;
+	/**
+	 * The `Store` to use to store the hit count for each client.
+	 *
+	 * By default, the built-in `MemoryStore` will be used.
+	 */
+	store: Store | LegacyStore;
+	/**
+	 * The list of validation checks that should run.
+	 */
+	validate: boolean | EnabledValidations;
+	/**
+	 * Whether to send `X-RateLimit-*` headers with the rate limit and the number
+	 * of requests.
+	 *
+	 * @deprecated 6.x - This option was renamed to `legacyHeaders`.
+	 */
+	headers?: boolean;
+	/**
+	 * The maximum number of connections to allow during the `window` before
+	 * rate limiting the client.
+	 *
+	 * Can be the limit itself as a number or express middleware that parses
+	 * the request and then figures out the limit.
+	 *
+	 * @deprecated 7.x - This option was renamed to `limit`. However, it will not
+	 * be removed from the library in the foreseeable future.
+	 */
+	max?: number | ValueDeterminingMiddleware<number>;
+	/**
+	 * If the Store generates an error, allow the request to pass.
+	 */
+	passOnStoreError: boolean;
+};
+/**
+ * The extended request object that includes information about the client's
+ * rate limit.
+ */
+export type AugmentedRequest = Request & {
+	[key: string]: RateLimitInfo;
+};
+/**
+ * The rate limit related information for each client included in the
+ * Express request object.
+ */
+export type RateLimitInfo = {
+	limit: number;
+	used: number;
+	remaining: number;
+	resetTime: Date | undefined;
+};
+/**
+ *
+ * Create an instance of IP rate-limiting middleware for Express.
+ *
+ * @param passedOptions {Options} - Options to configure the rate limiter.
+ *
+ * @returns {RateLimitRequestHandler} - The middleware that rate-limits clients based on your configuration.
+ *
+ * @public
+ */
+export declare const rateLimit: (passedOptions?: Partial<Options>) => RateLimitRequestHandler;
+/**
+ * The record that stores information about a client - namely, how many times
+ * they have hit the endpoint, and when their hit count resets.
+ *
+ * Similar to `ClientRateLimitInfo`, except `resetTime` is a compulsory field.
+ */
+export type Client = {
+	totalHits: number;
+	resetTime: Date;
+};
+/**
+ * A `Store` that stores the hit count for each client in memory.
+ *
+ * @public
+ */
+export declare class MemoryStore implements Store {
+	/**
+	 * The duration of time before which all hit counts are reset (in milliseconds).
+	 */
+	windowMs: number;
+	/**
+	 * These two maps store usage (requests) and reset time by key (for example, IP
+	 * addresses or API keys).
+	 *
+	 * They are split into two to avoid having to iterate through the entire set to
+	 * determine which ones need reset. Instead, `Client`s are moved from `previous`
+	 * to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients
+	 * left in `previous`, i.e., those that have not made any recent requests, are
+	 * known to be expired and can be deleted in bulk.
+	 */
+	previous: Map<string, Client>;
+	current: Map<string, Client>;
+	/**
+	 * A reference to the active timer.
+	 */
+	interval?: NodeJS.Timeout;
+	/**
+	 * Confirmation that the keys incremented in once instance of MemoryStore
+	 * cannot affect other instances.
+	 */
+	localKeys: boolean;
+	/**
+	 * Method that initializes the store.
+	 *
+	 * @param options {Options} - The options used to setup the middleware.
+	 */
+	init(options: Options): void;
+	/**
+	 * Method to fetch a client's hit count and reset time.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 *
+	 * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
+	 *
+	 * @public
+	 */
+	get(key: string): Promise<ClientRateLimitInfo | undefined>;
+	/**
+	 * Method to increment a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 *
+	 * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
+	 *
+	 * @public
+	 */
+	increment(key: string): Promise<ClientRateLimitInfo>;
+	/**
+	 * Method to decrement a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 *
+	 * @public
+	 */
+	decrement(key: string): Promise<void>;
+	/**
+	 * Method to reset a client's hit counter.
+	 *
+	 * @param key {string} - The identifier for a client.
+	 *
+	 * @public
+	 */
+	resetKey(key: string): Promise<void>;
+	/**
+	 * Method to reset everyone's hit counter.
+	 *
+	 * @public
+	 */
+	resetAll(): Promise<void>;
+	/**
+	 * Method to stop the timer (if currently running) and prevent any memory
+	 * leaks.
+	 *
+	 * @public
+	 */
+	shutdown(): void;
+	/**
+	 * Recycles a client by setting its hit count to zero, and reset time to
+	 * `windowMs` milliseconds from now.
+	 *
+	 * NOT to be confused with `#resetKey()`, which removes a client from both the
+	 * `current` and `previous` maps.
+	 *
+	 * @param client {Client} - The client to recycle.
+	 * @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client.
+	 *
+	 * @return {Client} - The modified client that was passed in, to allow for chaining.
+	 */
+	private resetClient;
+	/**
+	 * Retrieves or creates a client, given a key. Also ensures that the client being
+	 * returned is in the `current` map.
+	 *
+	 * @param key {string} - The key under which the client is (or is to be) stored.
+	 *
+	 * @returns {Client} - The requested client.
+	 */
+	private getClient;
+	/**
+	 * Move current clients to previous, create a new map for current.
+	 *
+	 * This function is called every `windowMs`.
+	 */
+	private clearExpired;
+}
+
+export {
+	rateLimit as default,
+};
+
+export {};
diff --git a/node_modules/express-rate-limit/dist/index.mjs b/node_modules/express-rate-limit/dist/index.mjs
new file mode 100644
index 0000000..f8b70ce
--- /dev/null
+++ b/node_modules/express-rate-limit/dist/index.mjs
@@ -0,0 +1,809 @@
+// source/headers.ts
+import { Buffer } from "buffer";
+import { createHash } from "crypto";
+var SUPPORTED_DRAFT_VERSIONS = ["draft-6", "draft-7", "draft-8"];
+var getResetSeconds = (resetTime, windowMs) => {
+  let resetSeconds = void 0;
+  if (resetTime) {
+    const deltaSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1e3);
+    resetSeconds = Math.max(0, deltaSeconds);
+  } else if (windowMs) {
+    resetSeconds = Math.ceil(windowMs / 1e3);
+  }
+  return resetSeconds;
+};
+var getPartitionKey = (key) => {
+  const hash = createHash("sha256");
+  hash.update(key);
+  const partitionKey = hash.digest("hex").slice(0, 12);
+  return Buffer.from(partitionKey).toString("base64");
+};
+var setLegacyHeaders = (response, info) => {
+  if (response.headersSent)
+    return;
+  response.setHeader("X-RateLimit-Limit", info.limit.toString());
+  response.setHeader("X-RateLimit-Remaining", info.remaining.toString());
+  if (info.resetTime instanceof Date) {
+    response.setHeader("Date", (/* @__PURE__ */ new Date()).toUTCString());
+    response.setHeader(
+      "X-RateLimit-Reset",
+      Math.ceil(info.resetTime.getTime() / 1e3).toString()
+    );
+  }
+};
+var setDraft6Headers = (response, info, windowMs) => {
+  if (response.headersSent)
+    return;
+  const windowSeconds = Math.ceil(windowMs / 1e3);
+  const resetSeconds = getResetSeconds(info.resetTime);
+  response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
+  response.setHeader("RateLimit-Limit", info.limit.toString());
+  response.setHeader("RateLimit-Remaining", info.remaining.toString());
+  if (resetSeconds)
+    response.setHeader("RateLimit-Reset", resetSeconds.toString());
+};
+var setDraft7Headers = (response, info, windowMs) => {
+  if (response.headersSent)
+    return;
+  const windowSeconds = Math.ceil(windowMs / 1e3);
+  const resetSeconds = getResetSeconds(info.resetTime, windowMs);
+  response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
+  response.setHeader(
+    "RateLimit",
+    `limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}`
+  );
+};
+var setDraft8Headers = (response, info, windowMs, name, key) => {
+  if (response.headersSent)
+    return;
+  const windowSeconds = Math.ceil(windowMs / 1e3);
+  const resetSeconds = getResetSeconds(info.resetTime, windowMs);
+  const partitionKey = getPartitionKey(key);
+  const policy = `q=${info.limit}; w=${windowSeconds}; pk=:${partitionKey}:`;
+  const header = `r=${info.remaining}; t=${resetSeconds}`;
+  response.append("RateLimit-Policy", `"${name}"; ${policy}`);
+  response.append("RateLimit", `"${name}"; ${header}`);
+};
+var setRetryAfterHeader = (response, info, windowMs) => {
+  if (response.headersSent)
+    return;
+  const resetSeconds = getResetSeconds(info.resetTime, windowMs);
+  response.setHeader("Retry-After", resetSeconds.toString());
+};
+
+// source/validations.ts
+import { isIP } from "net";
+var ValidationError = class extends Error {
+  /**
+   * The code must be a string, in snake case and all capital, that starts with
+   * the substring `ERR_ERL_`.
+   *
+   * The message must be a string, starting with an uppercase character,
+   * describing the issue in detail.
+   */
+  constructor(code, message) {
+    const url = `https://express-rate-limit.github.io/${code}/`;
+    super(`${message} See ${url} for more information.`);
+    this.name = this.constructor.name;
+    this.code = code;
+    this.help = url;
+  }
+};
+var ChangeWarning = class extends ValidationError {
+};
+var usedStores = /* @__PURE__ */ new Set();
+var singleCountKeys = /* @__PURE__ */ new WeakMap();
+var validations = {
+  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
+  enabled: {
+    default: true
+  },
+  // Should be EnabledValidations type, but that's a circular reference
+  disable() {
+    for (const k of Object.keys(this.enabled))
+      this.enabled[k] = false;
+  },
+  /**
+   * Checks whether the IP address is valid, and that it does not have a port
+   * number in it.
+   *
+   * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_invalid_ip_address.
+   *
+   * @param ip {string | undefined} - The IP address provided by Express as request.ip.
+   *
+   * @returns {void}
+   */
+  ip(ip) {
+    if (ip === void 0) {
+      throw new ValidationError(
+        "ERR_ERL_UNDEFINED_IP_ADDRESS",
+        `An undefined 'request.ip' was detected. This might indicate a misconfiguration or the connection being destroyed prematurely.`
+      );
+    }
+    if (!isIP(ip)) {
+      throw new ValidationError(
+        "ERR_ERL_INVALID_IP_ADDRESS",
+        `An invalid 'request.ip' (${ip}) was detected. Consider passing a custom 'keyGenerator' function to the rate limiter.`
+      );
+    }
+  },
+  /**
+   * Makes sure the trust proxy setting is not set to `true`.
+   *
+   * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_permissive_trust_proxy.
+   *
+   * @param request {Request} - The Express request object.
+   *
+   * @returns {void}
+   */
+  trustProxy(request) {
+    if (request.app.get("trust proxy") === true) {
+      throw new ValidationError(
+        "ERR_ERL_PERMISSIVE_TRUST_PROXY",
+        `The Express 'trust proxy' setting is true, which allows anyone to trivially bypass IP-based rate limiting.`
+      );
+    }
+  },
+  /**
+   * Makes sure the trust proxy setting is set in case the `X-Forwarded-For`
+   * header is present.
+   *
+   * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_unset_trust_proxy.
+   *
+   * @param request {Request} - The Express request object.
+   *
+   * @returns {void}
+   */
+  xForwardedForHeader(request) {
+    if (request.headers["x-forwarded-for"] && request.app.get("trust proxy") === false) {
+      throw new ValidationError(
+        "ERR_ERL_UNEXPECTED_X_FORWARDED_FOR",
+        `The 'X-Forwarded-For' header is set but the Express 'trust proxy' setting is false (default). This could indicate a misconfiguration which would prevent express-rate-limit from accurately identifying users.`
+      );
+    }
+  },
+  /**
+   * Ensures totalHits value from store is a positive integer.
+   *
+   * @param hits {any} - The `totalHits` returned by the store.
+   */
+  positiveHits(hits) {
+    if (typeof hits !== "number" || hits < 1 || hits !== Math.round(hits)) {
+      throw new ValidationError(
+        "ERR_ERL_INVALID_HITS",
+        `The totalHits value returned from the store must be a positive integer, got ${hits}`
+      );
+    }
+  },
+  /**
+   * Ensures a single store instance is not used with multiple express-rate-limit instances
+   */
+  unsharedStore(store) {
+    if (usedStores.has(store)) {
+      const maybeUniquePrefix = store?.localKeys ? "" : " (with a unique prefix)";
+      throw new ValidationError(
+        "ERR_ERL_STORE_REUSE",
+        `A Store instance must not be shared across multiple rate limiters. Create a new instance of ${store.constructor.name}${maybeUniquePrefix} for each limiter instead.`
+      );
+    }
+    usedStores.add(store);
+  },
+  /**
+   * Ensures a given key is incremented only once per request.
+   *
+   * @param request {Request} - The Express request object.
+   * @param store {Store} - The store class.
+   * @param key {string} - The key used to store the client's hit count.
+   *
+   * @returns {void}
+   */
+  singleCount(request, store, key) {
+    let storeKeys = singleCountKeys.get(request);
+    if (!storeKeys) {
+      storeKeys = /* @__PURE__ */ new Map();
+      singleCountKeys.set(request, storeKeys);
+    }
+    const storeKey = store.localKeys ? store : store.constructor.name;
+    let keys = storeKeys.get(storeKey);
+    if (!keys) {
+      keys = [];
+      storeKeys.set(storeKey, keys);
+    }
+    const prefixedKey = `${store.prefix ?? ""}${key}`;
+    if (keys.includes(prefixedKey)) {
+      throw new ValidationError(
+        "ERR_ERL_DOUBLE_COUNT",
+        `The hit count for ${key} was incremented more than once for a single request.`
+      );
+    }
+    keys.push(prefixedKey);
+  },
+  /**
+   * Warns the user that the behaviour for `max: 0` / `limit: 0` is
+   * changing in the next major release.
+   *
+   * @param limit {number} - The maximum number of hits per client.
+   *
+   * @returns {void}
+   */
+  limit(limit) {
+    if (limit === 0) {
+      throw new ChangeWarning(
+        "WRN_ERL_MAX_ZERO",
+        `Setting limit or max to 0 disables rate limiting in express-rate-limit v6 and older, but will cause all requests to be blocked in v7`
+      );
+    }
+  },
+  /**
+   * Warns the user that the `draft_polli_ratelimit_headers` option is deprecated
+   * and will be removed in the next major release.
+   *
+   * @param draft_polli_ratelimit_headers {any | undefined} - The now-deprecated setting that was used to enable standard headers.
+   *
+   * @returns {void}
+   */
+  draftPolliHeaders(draft_polli_ratelimit_headers) {
+    if (draft_polli_ratelimit_headers) {
+      throw new ChangeWarning(
+        "WRN_ERL_DEPRECATED_DRAFT_POLLI_HEADERS",
+        `The draft_polli_ratelimit_headers configuration option is deprecated and has been removed in express-rate-limit v7, please set standardHeaders: 'draft-6' instead.`
+      );
+    }
+  },
+  /**
+   * Warns the user that the `onLimitReached` option is deprecated and
+   * will be removed in the next major release.
+   *
+   * @param onLimitReached {any | undefined} - The maximum number of hits per client.
+   *
+   * @returns {void}
+   */
+  onLimitReached(onLimitReached) {
+    if (onLimitReached) {
+      throw new ChangeWarning(
+        "WRN_ERL_DEPRECATED_ON_LIMIT_REACHED",
+        `The onLimitReached configuration option is deprecated and has been removed in express-rate-limit v7.`
+      );
+    }
+  },
+  /**
+   * Warns the user when an invalid/unsupported version of the draft spec is passed.
+   *
+   * @param version {any | undefined} - The version passed by the user.
+   *
+   * @returns {void}
+   */
+  headersDraftVersion(version) {
+    if (typeof version !== "string" || !SUPPORTED_DRAFT_VERSIONS.includes(version)) {
+      const versionString = SUPPORTED_DRAFT_VERSIONS.join(", ");
+      throw new ValidationError(
+        "ERR_ERL_HEADERS_UNSUPPORTED_DRAFT_VERSION",
+        `standardHeaders: only the following versions of the IETF draft specification are supported: ${versionString}.`
+      );
+    }
+  },
+  /**
+   * Warns the user when the selected headers option requires a reset time but
+   * the store does not provide one.
+   *
+   * @param resetTime {Date | undefined} - The timestamp when the client's hit count will be reset.
+   *
+   * @returns {void}
+   */
+  headersResetTime(resetTime) {
+    if (!resetTime) {
+      throw new ValidationError(
+        "ERR_ERL_HEADERS_NO_RESET",
+        `standardHeaders:  'draft-7' requires a 'resetTime', but the store did not provide one. The 'windowMs' value will be used instead, which may cause clients to wait longer than necessary.`
+      );
+    }
+  },
+  /**
+   * Checks the options.validate setting to ensure that only recognized
+   * validations are enabled or disabled.
+   *
+   * If any unrecognized values are found, an error is logged that
+   * includes the list of supported vaidations.
+   */
+  validationsConfig() {
+    const supportedValidations = Object.keys(this).filter(
+      (k) => !["enabled", "disable"].includes(k)
+    );
+    supportedValidations.push("default");
+    for (const key of Object.keys(this.enabled)) {
+      if (!supportedValidations.includes(key)) {
+        throw new ValidationError(
+          "ERR_ERL_UNKNOWN_VALIDATION",
+          `options.validate.${key} is not recognized. Supported validate options are: ${supportedValidations.join(
+            ", "
+          )}.`
+        );
+      }
+    }
+  },
+  /**
+   * Checks to see if the instance was created inside of a request handler,
+   * which would prevent it from working correctly, with the default memory
+   * store (or any other store with localKeys.)
+   */
+  creationStack(store) {
+    const { stack } = new Error(
+      "express-rate-limit validation check (set options.validate.creationStack=false to disable)"
+    );
+    if (stack?.includes("Layer.handle [as handle_request]")) {
+      if (!store.localKeys) {
+        throw new ValidationError(
+          "ERR_ERL_CREATED_IN_REQUEST_HANDLER",
+          "express-rate-limit instance should *usually* be created at app initialization, not when responding to a request."
+        );
+      }
+      throw new ValidationError(
+        "ERR_ERL_CREATED_IN_REQUEST_HANDLER",
+        `express-rate-limit instance should be created at app initialization, not when responding to a request.`
+      );
+    }
+  }
+};
+var getValidations = (_enabled) => {
+  let enabled;
+  if (typeof _enabled === "boolean") {
+    enabled = {
+      default: _enabled
+    };
+  } else {
+    enabled = {
+      default: true,
+      ..._enabled
+    };
+  }
+  const wrappedValidations = {
+    enabled
+  };
+  for (const [name, validation] of Object.entries(validations)) {
+    if (typeof validation === "function")
+      wrappedValidations[name] = (...args) => {
+        if (!(enabled[name] ?? enabled.default)) {
+          return;
+        }
+        try {
+          ;
+          validation.apply(
+            wrappedValidations,
+            args
+          );
+        } catch (error) {
+          if (error instanceof ChangeWarning)
+            console.warn(error);
+          else
+            console.error(error);
+        }
+      };
+  }
+  return wrappedValidations;
+};
+
+// source/memory-store.ts
+var MemoryStore = class {
+  constructor() {
+    /**
+     * These two maps store usage (requests) and reset time by key (for example, IP
+     * addresses or API keys).
+     *
+     * They are split into two to avoid having to iterate through the entire set to
+     * determine which ones need reset. Instead, `Client`s are moved from `previous`
+     * to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients
+     * left in `previous`, i.e., those that have not made any recent requests, are
+     * known to be expired and can be deleted in bulk.
+     */
+    this.previous = /* @__PURE__ */ new Map();
+    this.current = /* @__PURE__ */ new Map();
+    /**
+     * Confirmation that the keys incremented in once instance of MemoryStore
+     * cannot affect other instances.
+     */
+    this.localKeys = true;
+  }
+  /**
+   * Method that initializes the store.
+   *
+   * @param options {Options} - The options used to setup the middleware.
+   */
+  init(options) {
+    this.windowMs = options.windowMs;
+    if (this.interval)
+      clearInterval(this.interval);
+    this.interval = setInterval(() => {
+      this.clearExpired();
+    }, this.windowMs);
+    if (this.interval.unref)
+      this.interval.unref();
+  }
+  /**
+   * Method to fetch a client's hit count and reset time.
+   *
+   * @param key {string} - The identifier for a client.
+   *
+   * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
+   *
+   * @public
+   */
+  async get(key) {
+    return this.current.get(key) ?? this.previous.get(key);
+  }
+  /**
+   * Method to increment a client's hit counter.
+   *
+   * @param key {string} - The identifier for a client.
+   *
+   * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
+   *
+   * @public
+   */
+  async increment(key) {
+    const client = this.getClient(key);
+    const now = Date.now();
+    if (client.resetTime.getTime() <= now) {
+      this.resetClient(client, now);
+    }
+    client.totalHits++;
+    return client;
+  }
+  /**
+   * Method to decrement a client's hit counter.
+   *
+   * @param key {string} - The identifier for a client.
+   *
+   * @public
+   */
+  async decrement(key) {
+    const client = this.getClient(key);
+    if (client.totalHits > 0)
+      client.totalHits--;
+  }
+  /**
+   * Method to reset a client's hit counter.
+   *
+   * @param key {string} - The identifier for a client.
+   *
+   * @public
+   */
+  async resetKey(key) {
+    this.current.delete(key);
+    this.previous.delete(key);
+  }
+  /**
+   * Method to reset everyone's hit counter.
+   *
+   * @public
+   */
+  async resetAll() {
+    this.current.clear();
+    this.previous.clear();
+  }
+  /**
+   * Method to stop the timer (if currently running) and prevent any memory
+   * leaks.
+   *
+   * @public
+   */
+  shutdown() {
+    clearInterval(this.interval);
+    void this.resetAll();
+  }
+  /**
+   * Recycles a client by setting its hit count to zero, and reset time to
+   * `windowMs` milliseconds from now.
+   *
+   * NOT to be confused with `#resetKey()`, which removes a client from both the
+   * `current` and `previous` maps.
+   *
+   * @param client {Client} - The client to recycle.
+   * @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client.
+   *
+   * @return {Client} - The modified client that was passed in, to allow for chaining.
+   */
+  resetClient(client, now = Date.now()) {
+    client.totalHits = 0;
+    client.resetTime.setTime(now + this.windowMs);
+    return client;
+  }
+  /**
+   * Retrieves or creates a client, given a key. Also ensures that the client being
+   * returned is in the `current` map.
+   *
+   * @param key {string} - The key under which the client is (or is to be) stored.
+   *
+   * @returns {Client} - The requested client.
+   */
+  getClient(key) {
+    if (this.current.has(key))
+      return this.current.get(key);
+    let client;
+    if (this.previous.has(key)) {
+      client = this.previous.get(key);
+      this.previous.delete(key);
+    } else {
+      client = { totalHits: 0, resetTime: /* @__PURE__ */ new Date() };
+      this.resetClient(client);
+    }
+    this.current.set(key, client);
+    return client;
+  }
+  /**
+   * Move current clients to previous, create a new map for current.
+   *
+   * This function is called every `windowMs`.
+   */
+  clearExpired() {
+    this.previous = this.current;
+    this.current = /* @__PURE__ */ new Map();
+  }
+};
+
+// source/lib.ts
+var isLegacyStore = (store) => (
+  // Check that `incr` exists but `increment` does not - store authors might want
+  // to keep both around for backwards compatibility.
+  typeof store.incr === "function" && typeof store.increment !== "function"
+);
+var promisifyStore = (passedStore) => {
+  if (!isLegacyStore(passedStore)) {
+    return passedStore;
+  }
+  const legacyStore = passedStore;
+  class PromisifiedStore {
+    async increment(key) {
+      return new Promise((resolve, reject) => {
+        legacyStore.incr(
+          key,
+          (error, totalHits, resetTime) => {
+            if (error)
+              reject(error);
+            resolve({ totalHits, resetTime });
+          }
+        );
+      });
+    }
+    async decrement(key) {
+      return legacyStore.decrement(key);
+    }
+    async resetKey(key) {
+      return legacyStore.resetKey(key);
+    }
+    /* istanbul ignore next */
+    async resetAll() {
+      if (typeof legacyStore.resetAll === "function")
+        return legacyStore.resetAll();
+    }
+  }
+  return new PromisifiedStore();
+};
+var getOptionsFromConfig = (config) => {
+  const { validations: validations2, ...directlyPassableEntries } = config;
+  return {
+    ...directlyPassableEntries,
+    validate: validations2.enabled
+  };
+};
+var omitUndefinedOptions = (passedOptions) => {
+  const omittedOptions = {};
+  for (const k of Object.keys(passedOptions)) {
+    const key = k;
+    if (passedOptions[key] !== void 0) {
+      omittedOptions[key] = passedOptions[key];
+    }
+  }
+  return omittedOptions;
+};
+var parseOptions = (passedOptions) => {
+  const notUndefinedOptions = omitUndefinedOptions(passedOptions);
+  const validations2 = getValidations(notUndefinedOptions?.validate ?? true);
+  validations2.validationsConfig();
+  validations2.draftPolliHeaders(
+    // @ts-expect-error see the note above.
+    notUndefinedOptions.draft_polli_ratelimit_headers
+  );
+  validations2.onLimitReached(notUndefinedOptions.onLimitReached);
+  let standardHeaders = notUndefinedOptions.standardHeaders ?? false;
+  if (standardHeaders === true)
+    standardHeaders = "draft-6";
+  const config = {
+    windowMs: 60 * 1e3,
+    limit: passedOptions.max ?? 5,
+    // `max` is deprecated, but support it anyways.
+    message: "Too many requests, please try again later.",
+    statusCode: 429,
+    legacyHeaders: passedOptions.headers ?? true,
+    identifier(request, _response) {
+      let duration = "";
+      const property = config.requestPropertyName;
+      const { limit } = request[property];
+      const seconds = config.windowMs / 1e3;
+      const minutes = config.windowMs / (1e3 * 60);
+      const hours = config.windowMs / (1e3 * 60 * 60);
+      const days = config.windowMs / (1e3 * 60 * 60 * 24);
+      if (seconds < 60)
+        duration = `${seconds}sec`;
+      else if (minutes < 60)
+        duration = `${minutes}min`;
+      else if (hours < 24)
+        duration = `${hours}hr${hours > 1 ? "s" : ""}`;
+      else
+        duration = `${days}day${days > 1 ? "s" : ""}`;
+      return `${limit}-in-${duration}`;
+    },
+    requestPropertyName: "rateLimit",
+    skipFailedRequests: false,
+    skipSuccessfulRequests: false,
+    requestWasSuccessful: (_request, response) => response.statusCode < 400,
+    skip: (_request, _response) => false,
+    keyGenerator(request, _response) {
+      validations2.ip(request.ip);
+      validations2.trustProxy(request);
+      validations2.xForwardedForHeader(request);
+      return request.ip;
+    },
+    async handler(request, response, _next, _optionsUsed) {
+      response.status(config.statusCode);
+      const message = typeof config.message === "function" ? await config.message(
+        request,
+        response
+      ) : config.message;
+      if (!response.writableEnded) {
+        response.send(message);
+      }
+    },
+    passOnStoreError: false,
+    // Allow the default options to be overriden by the passed options.
+    ...notUndefinedOptions,
+    // `standardHeaders` is resolved into a draft version above, use that.
+    standardHeaders,
+    // Note that this field is declared after the user's options are spread in,
+    // so that this field doesn't get overriden with an un-promisified store!
+    store: promisifyStore(notUndefinedOptions.store ?? new MemoryStore()),
+    // Print an error to the console if a few known misconfigurations are detected.
+    validations: validations2
+  };
+  if (typeof config.store.increment !== "function" || typeof config.store.decrement !== "function" || typeof config.store.resetKey !== "function" || config.store.resetAll !== void 0 && typeof config.store.resetAll !== "function" || config.store.init !== void 0 && typeof config.store.init !== "function") {
+    throw new TypeError(
+      "An invalid store was passed. Please ensure that the store is a class that implements the `Store` interface."
+    );
+  }
+  return config;
+};
+var handleAsyncErrors = (fn) => async (request, response, next) => {
+  try {
+    await Promise.resolve(fn(request, response, next)).catch(next);
+  } catch (error) {
+    next(error);
+  }
+};
+var rateLimit = (passedOptions) => {
+  const config = parseOptions(passedOptions ?? {});
+  const options = getOptionsFromConfig(config);
+  config.validations.creationStack(config.store);
+  config.validations.unsharedStore(config.store);
+  if (typeof config.store.init === "function")
+    config.store.init(options);
+  const middleware = handleAsyncErrors(
+    async (request, response, next) => {
+      const skip = await config.skip(request, response);
+      if (skip) {
+        next();
+        return;
+      }
+      const augmentedRequest = request;
+      const key = await config.keyGenerator(request, response);
+      let totalHits = 0;
+      let resetTime;
+      try {
+        const incrementResult = await config.store.increment(key);
+        totalHits = incrementResult.totalHits;
+        resetTime = incrementResult.resetTime;
+      } catch (error) {
+        if (config.passOnStoreError) {
+          console.error(
+            "express-rate-limit: error from store, allowing request without rate-limiting.",
+            error
+          );
+          next();
+          return;
+        }
+        throw error;
+      }
+      config.validations.positiveHits(totalHits);
+      config.validations.singleCount(request, config.store, key);
+      const retrieveLimit = typeof config.limit === "function" ? config.limit(request, response) : config.limit;
+      const limit = await retrieveLimit;
+      config.validations.limit(limit);
+      const info = {
+        limit,
+        used: totalHits,
+        remaining: Math.max(limit - totalHits, 0),
+        resetTime
+      };
+      Object.defineProperty(info, "current", {
+        configurable: false,
+        enumerable: false,
+        value: totalHits
+      });
+      augmentedRequest[config.requestPropertyName] = info;
+      if (config.legacyHeaders && !response.headersSent) {
+        setLegacyHeaders(response, info);
+      }
+      if (config.standardHeaders && !response.headersSent) {
+        switch (config.standardHeaders) {
+          case "draft-6": {
+            setDraft6Headers(response, info, config.windowMs);
+            break;
+          }
+          case "draft-7": {
+            config.validations.headersResetTime(info.resetTime);
+            setDraft7Headers(response, info, config.windowMs);
+            break;
+          }
+          case "draft-8": {
+            const retrieveName = typeof config.identifier === "function" ? config.identifier(request, response) : config.identifier;
+            const name = await retrieveName;
+            config.validations.headersResetTime(info.resetTime);
+            setDraft8Headers(response, info, config.windowMs, name, key);
+            break;
+          }
+          default: {
+            config.validations.headersDraftVersion(config.standardHeaders);
+            break;
+          }
+        }
+      }
+      if (config.skipFailedRequests || config.skipSuccessfulRequests) {
+        let decremented = false;
+        const decrementKey = async () => {
+          if (!decremented) {
+            await config.store.decrement(key);
+            decremented = true;
+          }
+        };
+        if (config.skipFailedRequests) {
+          response.on("finish", async () => {
+            if (!await config.requestWasSuccessful(request, response))
+              await decrementKey();
+          });
+          response.on("close", async () => {
+            if (!response.writableEnded)
+              await decrementKey();
+          });
+          response.on("error", async () => {
+            await decrementKey();
+          });
+        }
+        if (config.skipSuccessfulRequests) {
+          response.on("finish", async () => {
+            if (await config.requestWasSuccessful(request, response))
+              await decrementKey();
+          });
+        }
+      }
+      config.validations.disable();
+      if (totalHits > limit) {
+        if (config.legacyHeaders || config.standardHeaders) {
+          setRetryAfterHeader(response, info, config.windowMs);
+        }
+        config.handler(request, response, next, options);
+        return;
+      }
+      next();
+    }
+  );
+  const getThrowFn = () => {
+    throw new Error("The current store does not support the get/getKey method");
+  };
+  middleware.resetKey = config.store.resetKey.bind(config.store);
+  middleware.getKey = typeof config.store.get === "function" ? config.store.get.bind(config.store) : getThrowFn;
+  return middleware;
+};
+var lib_default = rateLimit;
+export {
+  MemoryStore,
+  lib_default as default,
+  lib_default as rateLimit
+};
diff --git a/node_modules/express-rate-limit/package.json b/node_modules/express-rate-limit/package.json
new file mode 100644
index 0000000..1783725
--- /dev/null
+++ b/node_modules/express-rate-limit/package.json
@@ -0,0 +1,133 @@
+{
+	"name": "express-rate-limit",
+	"version": "7.5.0",
+	"description": "Basic IP rate-limiting middleware for Express. Use to limit repeated requests to public APIs and/or endpoints such as password reset.",
+	"author": {
+		"name": "Nathan Friedly",
+		"url": "http://nfriedly.com/"
+	},
+	"license": "MIT",
+	"homepage": "https://github.com/express-rate-limit/express-rate-limit",
+	"repository": {
+		"type": "git",
+		"url": "git+https://github.com/express-rate-limit/express-rate-limit.git"
+	},
+	"funding": "https://github.com/sponsors/express-rate-limit",
+	"keywords": [
+		"express-rate-limit",
+		"express",
+		"rate",
+		"limit",
+		"ratelimit",
+		"rate-limit",
+		"middleware",
+		"ip",
+		"auth",
+		"authorization",
+		"security",
+		"brute",
+		"force",
+		"bruteforce",
+		"brute-force",
+		"attack"
+	],
+	"type": "module",
+	"exports": {
+		".": {
+			"import": {
+				"types": "./dist/index.d.mts",
+				"default": "./dist/index.mjs"
+			},
+			"require": {
+				"types": "./dist/index.d.cts",
+				"default": "./dist/index.cjs"
+			}
+		}
+	},
+	"main": "./dist/index.cjs",
+	"module": "./dist/index.mjs",
+	"types": "./dist/index.d.ts",
+	"files": [
+		"dist/",
+		"tsconfig.json"
+	],
+	"engines": {
+		"node": ">= 16"
+	},
+	"scripts": {
+		"clean": "del-cli dist/ coverage/ *.log *.tmp *.bak *.tgz",
+		"build:cjs": "esbuild --platform=node --bundle --target=es2022 --format=cjs --outfile=dist/index.cjs --footer:js=\"module.exports = rateLimit; module.exports.default = rateLimit; module.exports.rateLimit = rateLimit; module.exports.MemoryStore = MemoryStore;\" source/index.ts",
+		"build:esm": "esbuild --platform=node --bundle --target=es2022 --format=esm --outfile=dist/index.mjs source/index.ts",
+		"build:types": "dts-bundle-generator --out-file=dist/index.d.ts source/index.ts && cp dist/index.d.ts dist/index.d.cts && cp dist/index.d.ts dist/index.d.mts",
+		"compile": "run-s clean build:*",
+		"docs": "cd docs && mintlify dev",
+		"lint:code": "xo",
+		"lint:rest": "prettier --check .",
+		"lint": "run-s lint:*",
+		"format:code": "xo --fix",
+		"format:rest": "prettier --write .",
+		"format": "run-s format:*",
+		"test:lib": "jest",
+		"test:ext": "cd test/external/ && bash run-all-tests",
+		"test": "run-s lint test:lib",
+		"pre-commit": "lint-staged",
+		"prepare": "run-s compile && husky install config/husky"
+	},
+	"peerDependencies": {
+		"express": "^4.11 || 5 || ^5.0.0-beta.1"
+	},
+	"devDependencies": {
+		"@express-rate-limit/prettier": "1.1.1",
+		"@express-rate-limit/tsconfig": "1.0.2",
+		"@jest/globals": "29.7.0",
+		"@types/express": "4.17.20",
+		"@types/jest": "29.5.6",
+		"@types/node": "20.8.7",
+		"@types/supertest": "2.0.15",
+		"del-cli": "5.1.0",
+		"dts-bundle-generator": "8.0.1",
+		"esbuild": "0.19.5",
+		"express": "4.21.1",
+		"husky": "8.0.3",
+		"jest": "29.7.0",
+		"lint-staged": "15.0.2",
+		"mintlify": "4.0.63",
+		"npm-run-all": "4.1.5",
+		"ratelimit-header-parser": "0.1.0",
+		"supertest": "6.3.3",
+		"ts-jest": "29.1.1",
+		"ts-node": "10.9.1",
+		"typescript": "5.2.2",
+		"xo": "0.56.0"
+	},
+	"xo": {
+		"prettier": true,
+		"rules": {
+			"@typescript-eslint/no-empty-function": 0,
+			"@typescript-eslint/no-dynamic-delete": 0,
+			"@typescript-eslint/no-confusing-void-expression": 0,
+			"@typescript-eslint/consistent-indexed-object-style": [
+				"error",
+				"index-signature"
+			],
+			"n/no-unsupported-features/es-syntax": 0
+		},
+		"overrides": [
+			{
+				"files": "test/library/*.ts",
+				"rules": {
+					"@typescript-eslint/no-unsafe-argument": 0,
+					"@typescript-eslint/no-unsafe-assignment": 0
+				}
+			}
+		],
+		"ignore": [
+			"test/external"
+		]
+	},
+	"prettier": "@express-rate-limit/prettier",
+	"lint-staged": {
+		"{source,test}/**/*.ts": "xo --fix",
+		"**/*.{json,yaml,md}": "prettier --write "
+	}
+}
diff --git a/node_modules/express-rate-limit/tsconfig.json b/node_modules/express-rate-limit/tsconfig.json
new file mode 100644
index 0000000..52b6ff1
--- /dev/null
+++ b/node_modules/express-rate-limit/tsconfig.json
@@ -0,0 +1,8 @@
+{
+	"include": ["source/"],
+	"exclude": ["node_modules/"],
+	"extends": "@express-rate-limit/tsconfig",
+	"compilerOptions": {
+		"target": "ES2020"
+	}
+}
diff --git a/node_modules/express-session/index.js b/node_modules/express-session/index.js
new file mode 100644
index 0000000..d41b237
--- /dev/null
+++ b/node_modules/express-session/index.js
@@ -0,0 +1,693 @@
+/*!
+ * express-session
+ * Copyright(c) 2010 Sencha Inc.
+ * Copyright(c) 2011 TJ Holowaychuk
+ * Copyright(c) 2014-2015 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+'use strict';
+
+/**
+ * Module dependencies.
+ * @private
+ */
+
+var Buffer = require('safe-buffer').Buffer
+var cookie = require('cookie');
+var crypto = require('crypto')
+var debug = require('debug')('express-session');
+var deprecate = require('depd')('express-session');
+var onHeaders = require('on-headers')
+var parseUrl = require('parseurl');
+var signature = require('cookie-signature')
+var uid = require('uid-safe').sync
+
+var Cookie = require('./session/cookie')
+var MemoryStore = require('./session/memory')
+var Session = require('./session/session')
+var Store = require('./session/store')
+
+// environment
+
+var env = process.env.NODE_ENV;
+
+/**
+ * Expose the middleware.
+ */
+
+exports = module.exports = session;
+
+/**
+ * Expose constructors.
+ */
+
+exports.Store = Store;
+exports.Cookie = Cookie;
+exports.Session = Session;
+exports.MemoryStore = MemoryStore;
+
+/**
+ * Warning message for `MemoryStore` usage in production.
+ * @private
+ */
+
+var warning = 'Warning: connect.session() MemoryStore is not\n'
+  + 'designed for a production environment, as it will leak\n'
+  + 'memory, and will not scale past a single process.';
+
+/**
+ * Node.js 0.8+ async implementation.
+ * @private
+ */
+
+/* istanbul ignore next */
+var defer = typeof setImmediate === 'function'
+  ? setImmediate
+  : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) }
+
+/**
+ * Setup session store with the given `options`.
+ *
+ * @param {Object} [options]
+ * @param {Object} [options.cookie] Options for cookie
+ * @param {Function} [options.genid]
+ * @param {String} [options.name=connect.sid] Session ID cookie name
+ * @param {Boolean} [options.proxy]
+ * @param {Boolean} [options.resave] Resave unmodified sessions back to the store
+ * @param {Boolean} [options.rolling] Enable/disable rolling session expiration
+ * @param {Boolean} [options.saveUninitialized] Save uninitialized sessions to the store
+ * @param {String|Array} [options.secret] Secret for signing session ID
+ * @param {Object} [options.store=MemoryStore] Session store
+ * @param {String} [options.unset]
+ * @return {Function} middleware
+ * @public
+ */
+
+function session(options) {
+  var opts = options || {}
+
+  // get the cookie options
+  var cookieOptions = opts.cookie || {}
+
+  // get the session id generate function
+  var generateId = opts.genid || generateSessionId
+
+  // get the session cookie name
+  var name = opts.name || opts.key || 'connect.sid'
+
+  // get the session store
+  var store = opts.store || new MemoryStore()
+
+  // get the trust proxy setting
+  var trustProxy = opts.proxy
+
+  // get the resave session option
+  var resaveSession = opts.resave;
+
+  // get the rolling session option
+  var rollingSessions = Boolean(opts.rolling)
+
+  // get the save uninitialized session option
+  var saveUninitializedSession = opts.saveUninitialized
+
+  // get the cookie signing secret
+  var secret = opts.secret
+
+  if (typeof generateId !== 'function') {
+    throw new TypeError('genid option must be a function');
+  }
+
+  if (resaveSession === undefined) {
+    deprecate('undefined resave option; provide resave option');
+    resaveSession = true;
+  }
+
+  if (saveUninitializedSession === undefined) {
+    deprecate('undefined saveUninitialized option; provide saveUninitialized option');
+    saveUninitializedSession = true;
+  }
+
+  if (opts.unset && opts.unset !== 'destroy' && opts.unset !== 'keep') {
+    throw new TypeError('unset option must be "destroy" or "keep"');
+  }
+
+  // TODO: switch to "destroy" on next major
+  var unsetDestroy = opts.unset === 'destroy'
+
+  if (Array.isArray(secret) && secret.length === 0) {
+    throw new TypeError('secret option array must contain one or more strings');
+  }
+
+  if (secret && !Array.isArray(secret)) {
+    secret = [secret];
+  }
+
+  if (!secret) {
+    deprecate('req.secret; provide secret option');
+  }
+
+  // notify user that this store is not
+  // meant for a production environment
+  /* istanbul ignore next: not tested */
+  if (env === 'production' && store instanceof MemoryStore) {
+    console.warn(warning);
+  }
+
+  // generates the new session
+  store.generate = function(req){
+    req.sessionID = generateId(req);
+    req.session = new Session(req);
+    req.session.cookie = new Cookie(cookieOptions);
+
+    if (cookieOptions.secure === 'auto') {
+      req.session.cookie.secure = issecure(req, trustProxy);
+    }
+  };
+
+  var storeImplementsTouch = typeof store.touch === 'function';
+
+  // register event listeners for the store to track readiness
+  var storeReady = true
+  store.on('disconnect', function ondisconnect() {
+    storeReady = false
+  })
+  store.on('connect', function onconnect() {
+    storeReady = true
+  })
+
+  return function session(req, res, next) {
+    // self-awareness
+    if (req.session) {
+      next()
+      return
+    }
+
+    // Handle connection as if there is no session if
+    // the store has temporarily disconnected etc
+    if (!storeReady) {
+      debug('store is disconnected')
+      next()
+      return
+    }
+
+    // pathname mismatch
+    var originalPath = parseUrl.original(req).pathname || '/'
+    if (originalPath.indexOf(cookieOptions.path || '/') !== 0) {
+      debug('pathname mismatch')
+      next()
+      return
+    }
+
+    // ensure a secret is available or bail
+    if (!secret && !req.secret) {
+      next(new Error('secret option required for sessions'));
+      return;
+    }
+
+    // backwards compatibility for signed cookies
+    // req.secret is passed from the cookie parser middleware
+    var secrets = secret || [req.secret];
+
+    var originalHash;
+    var originalId;
+    var savedHash;
+    var touched = false
+
+    // expose store
+    req.sessionStore = store;
+
+    // get the session ID from the cookie
+    var cookieId = req.sessionID = getcookie(req, name, secrets);
+
+    // set-cookie
+    onHeaders(res, function(){
+      if (!req.session) {
+        debug('no session');
+        return;
+      }
+
+      if (!shouldSetCookie(req)) {
+        return;
+      }
+
+      // only send secure cookies via https
+      if (req.session.cookie.secure && !issecure(req, trustProxy)) {
+        debug('not secured');
+        return;
+      }
+
+      if (!touched) {
+        // touch session
+        req.session.touch()
+        touched = true
+      }
+
+      // set cookie
+      try {
+        setcookie(res, name, req.sessionID, secrets[0], req.session.cookie.data)
+      } catch (err) {
+        defer(next, err)
+      }
+    });
+
+    // proxy end() to commit the session
+    var _end = res.end;
+    var _write = res.write;
+    var ended = false;
+    res.end = function end(chunk, encoding) {
+      if (ended) {
+        return false;
+      }
+
+      ended = true;
+
+      var ret;
+      var sync = true;
+
+      function writeend() {
+        if (sync) {
+          ret = _end.call(res, chunk, encoding);
+          sync = false;
+          return;
+        }
+
+        _end.call(res);
+      }
+
+      function writetop() {
+        if (!sync) {
+          return ret;
+        }
+
+        if (!res._header) {
+          res._implicitHeader()
+        }
+
+        if (chunk == null) {
+          ret = true;
+          return ret;
+        }
+
+        var contentLength = Number(res.getHeader('Content-Length'));
+
+        if (!isNaN(contentLength) && contentLength > 0) {
+          // measure chunk
+          chunk = !Buffer.isBuffer(chunk)
+            ? Buffer.from(chunk, encoding)
+            : chunk;
+          encoding = undefined;
+
+          if (chunk.length !== 0) {
+            debug('split response');
+            ret = _write.call(res, chunk.slice(0, chunk.length - 1));
+            chunk = chunk.slice(chunk.length - 1, chunk.length);
+            return ret;
+          }
+        }
+
+        ret = _write.call(res, chunk, encoding);
+        sync = false;
+
+        return ret;
+      }
+
+      if (shouldDestroy(req)) {
+        // destroy session
+        debug('destroying');
+        store.destroy(req.sessionID, function ondestroy(err) {
+          if (err) {
+            defer(next, err);
+          }
+
+          debug('destroyed');
+          writeend();
+        });
+
+        return writetop();
+      }
+
+      // no session to save
+      if (!req.session) {
+        debug('no session');
+        return _end.call(res, chunk, encoding);
+      }
+
+      if (!touched) {
+        // touch session
+        req.session.touch()
+        touched = true
+      }
+
+      if (shouldSave(req)) {
+        req.session.save(function onsave(err) {
+          if (err) {
+            defer(next, err);
+          }
+
+          writeend();
+        });
+
+        return writetop();
+      } else if (storeImplementsTouch && shouldTouch(req)) {
+        // store implements touch method
+        debug('touching');
+        store.touch(req.sessionID, req.session, function ontouch(err) {
+          if (err) {
+            defer(next, err);
+          }
+
+          debug('touched');
+          writeend();
+        });
+
+        return writetop();
+      }
+
+      return _end.call(res, chunk, encoding);
+    };
+
+    // generate the session
+    function generate() {
+      store.generate(req);
+      originalId = req.sessionID;
+      originalHash = hash(req.session);
+      wrapmethods(req.session);
+    }
+
+    // inflate the session
+    function inflate (req, sess) {
+      store.createSession(req, sess)
+      originalId = req.sessionID
+      originalHash = hash(sess)
+
+      if (!resaveSession) {
+        savedHash = originalHash
+      }
+
+      wrapmethods(req.session)
+    }
+
+    function rewrapmethods (sess, callback) {
+      return function () {
+        if (req.session !== sess) {
+          wrapmethods(req.session)
+        }
+
+        callback.apply(this, arguments)
+      }
+    }
+
+    // wrap session methods
+    function wrapmethods(sess) {
+      var _reload = sess.reload
+      var _save = sess.save;
+
+      function reload(callback) {
+        debug('reloading %s', this.id)
+        _reload.call(this, rewrapmethods(this, callback))
+      }
+
+      function save() {
+        debug('saving %s', this.id);
+        savedHash = hash(this);
+        _save.apply(this, arguments);
+      }
+
+      Object.defineProperty(sess, 'reload', {
+        configurable: true,
+        enumerable: false,
+        value: reload,
+        writable: true
+      })
+
+      Object.defineProperty(sess, 'save', {
+        configurable: true,
+        enumerable: false,
+        value: save,
+        writable: true
+      });
+    }
+
+    // check if session has been modified
+    function isModified(sess) {
+      return originalId !== sess.id || originalHash !== hash(sess);
+    }
+
+    // check if session has been saved
+    function isSaved(sess) {
+      return originalId === sess.id && savedHash === hash(sess);
+    }
+
+    // determine if session should be destroyed
+    function shouldDestroy(req) {
+      return req.sessionID && unsetDestroy && req.session == null;
+    }
+
+    // determine if session should be saved to store
+    function shouldSave(req) {
+      // cannot set cookie without a session ID
+      if (typeof req.sessionID !== 'string') {
+        debug('session ignored because of bogus req.sessionID %o', req.sessionID);
+        return false;
+      }
+
+      return !saveUninitializedSession && !savedHash && cookieId !== req.sessionID
+        ? isModified(req.session)
+        : !isSaved(req.session)
+    }
+
+    // determine if session should be touched
+    function shouldTouch(req) {
+      // cannot set cookie without a session ID
+      if (typeof req.sessionID !== 'string') {
+        debug('session ignored because of bogus req.sessionID %o', req.sessionID);
+        return false;
+      }
+
+      return cookieId === req.sessionID && !shouldSave(req);
+    }
+
+    // determine if cookie should be set on response
+    function shouldSetCookie(req) {
+      // cannot set cookie without a session ID
+      if (typeof req.sessionID !== 'string') {
+        return false;
+      }
+
+      return cookieId !== req.sessionID
+        ? saveUninitializedSession || isModified(req.session)
+        : rollingSessions || req.session.cookie.expires != null && isModified(req.session);
+    }
+
+    // generate a session if the browser doesn't send a sessionID
+    if (!req.sessionID) {
+      debug('no SID sent, generating session');
+      generate();
+      next();
+      return;
+    }
+
+    // generate the session object
+    debug('fetching %s', req.sessionID);
+    store.get(req.sessionID, function(err, sess){
+      // error handling
+      if (err && err.code !== 'ENOENT') {
+        debug('error %j', err);
+        next(err)
+        return
+      }
+
+      try {
+        if (err || !sess) {
+          debug('no session found')
+          generate()
+        } else {
+          debug('session found')
+          inflate(req, sess)
+        }
+      } catch (e) {
+        next(e)
+        return
+      }
+
+      next()
+    });
+  };
+};
+
+/**
+ * Generate a session ID for a new session.
+ *
+ * @return {String}
+ * @private
+ */
+
+function generateSessionId(sess) {
+  return uid(24);
+}
+
+/**
+ * Get the session ID cookie from request.
+ *
+ * @return {string}
+ * @private
+ */
+
+function getcookie(req, name, secrets) {
+  var header = req.headers.cookie;
+  var raw;
+  var val;
+
+  // read from cookie header
+  if (header) {
+    var cookies = cookie.parse(header);
+
+    raw = cookies[name];
+
+    if (raw) {
+      if (raw.substr(0, 2) === 's:') {
+        val = unsigncookie(raw.slice(2), secrets);
+
+        if (val === false) {
+          debug('cookie signature invalid');
+          val = undefined;
+        }
+      } else {
+        debug('cookie unsigned')
+      }
+    }
+  }
+
+  // back-compat read from cookieParser() signedCookies data
+  if (!val && req.signedCookies) {
+    val = req.signedCookies[name];
+
+    if (val) {
+      deprecate('cookie should be available in req.headers.cookie');
+    }
+  }
+
+  // back-compat read from cookieParser() cookies data
+  if (!val && req.cookies) {
+    raw = req.cookies[name];
+
+    if (raw) {
+      if (raw.substr(0, 2) === 's:') {
+        val = unsigncookie(raw.slice(2), secrets);
+
+        if (val) {
+          deprecate('cookie should be available in req.headers.cookie');
+        }
+
+        if (val === false) {
+          debug('cookie signature invalid');
+          val = undefined;
+        }
+      } else {
+        debug('cookie unsigned')
+      }
+    }
+  }
+
+  return val;
+}
+
+/**
+ * Hash the given `sess` object omitting changes to `.cookie`.
+ *
+ * @param {Object} sess
+ * @return {String}
+ * @private
+ */
+
+function hash(sess) {
+  // serialize
+  var str = JSON.stringify(sess, function (key, val) {
+    // ignore sess.cookie property
+    if (this === sess && key === 'cookie') {
+      return
+    }
+
+    return val
+  })
+
+  // hash
+  return crypto
+    .createHash('sha1')
+    .update(str, 'utf8')
+    .digest('hex')
+}
+
+/**
+ * Determine if request is secure.
+ *
+ * @param {Object} req
+ * @param {Boolean} [trustProxy]
+ * @return {Boolean}
+ * @private
+ */
+
+function issecure(req, trustProxy) {
+  // socket is https server
+  if (req.connection && req.connection.encrypted) {
+    return true;
+  }
+
+  // do not trust proxy
+  if (trustProxy === false) {
+    return false;
+  }
+
+  // no explicit trust; try req.secure from express
+  if (trustProxy !== true) {
+    return req.secure === true
+  }
+
+  // read the proto from x-forwarded-proto header
+  var header = req.headers['x-forwarded-proto'] || '';
+  var index = header.indexOf(',');
+  var proto = index !== -1
+    ? header.substr(0, index).toLowerCase().trim()
+    : header.toLowerCase().trim()
+
+  return proto === 'https';
+}
+
+/**
+ * Set cookie on response.
+ *
+ * @private
+ */
+
+function setcookie(res, name, val, secret, options) {
+  var signed = 's:' + signature.sign(val, secret);
+  var data = cookie.serialize(name, signed, options);
+
+  debug('set-cookie %s', data);
+
+  var prev = res.getHeader('Set-Cookie') || []
+  var header = Array.isArray(prev) ? prev.concat(data) : [prev, data];
+
+  res.setHeader('Set-Cookie', header)
+}
+
+/**
+ * Verify and decode the given `val` with `secrets`.
+ *
+ * @param {String} val
+ * @param {Array} secrets
+ * @returns {String|Boolean}
+ * @private
+ */
+function unsigncookie(val, secrets) {
+  for (var i = 0; i < secrets.length; i++) {
+    var result = signature.unsign(val, secrets[i]);
+
+    if (result !== false) {
+      return result;
+    }
+  }
+
+  return false;
+}
diff --git a/node_modules/express-session/node_modules/cookie-signature/index.js b/node_modules/express-session/node_modules/cookie-signature/index.js
new file mode 100644
index 0000000..336d487
--- /dev/null
+++ b/node_modules/express-session/node_modules/cookie-signature/index.js
@@ -0,0 +1,51 @@
+/**
+ * Module dependencies.
+ */
+
+var crypto = require('crypto');
+
+/**
+ * Sign the given `val` with `secret`.
+ *
+ * @param {String} val
+ * @param {String|NodeJS.ArrayBufferView|crypto.KeyObject} secret
+ * @return {String}
+ * @api private
+ */
+
+exports.sign = function(val, secret){
+  if ('string' !== typeof val) throw new TypeError("Cookie value must be provided as a string.");
+  if (null == secret) throw new TypeError("Secret key must be provided.");
+  return val + '.' + crypto
+    .createHmac('sha256', secret)
+    .update(val)
+    .digest('base64')
+    .replace(/\=+$/, '');
+};
+
+/**
+ * Unsign and decode the given `val` with `secret`,
+ * returning `false` if the signature is invalid.
+ *
+ * @param {String} val
+ * @param {String|NodeJS.ArrayBufferView|crypto.KeyObject} secret
+ * @return {String|Boolean}
+ * @api private
+ */
+
+exports.unsign = function(val, secret){
+  if ('string' !== typeof val) throw new TypeError("Signed cookie string must be provided.");
+  if (null == secret) throw new TypeError("Secret key must be provided.");
+  var str = val.slice(0, val.lastIndexOf('.'))
+    , mac = exports.sign(str, secret);
+  
+  return sha1(mac) == sha1(val) ? str : false;
+};
+
+/**
+ * Private
+ */
+
+function sha1(str){
+  return crypto.createHash('sha1').update(str).digest('hex');
+}
diff --git a/node_modules/express-session/node_modules/cookie-signature/package.json b/node_modules/express-session/node_modules/cookie-signature/package.json
new file mode 100644
index 0000000..738487b
--- /dev/null
+++ b/node_modules/express-session/node_modules/cookie-signature/package.json
@@ -0,0 +1,18 @@
+{
+  "name": "cookie-signature",
+  "version": "1.0.7",
+  "description": "Sign and unsign cookies",
+  "keywords": ["cookie", "sign", "unsign"],
+  "author": "TJ Holowaychuk <tj@learnboost.com>",
+  "license": "MIT",
+  "repository": { "type": "git", "url": "https://github.com/visionmedia/node-cookie-signature.git"},
+  "dependencies": {},
+  "devDependencies": {
+    "mocha": "*",
+    "should": "*"
+  },
+  "scripts": {
+    "test": "mocha --require should --reporter spec"
+  },
+  "main": "index"
+}
\ No newline at end of file
diff --git a/node_modules/express-session/node_modules/cookie/index.js b/node_modules/express-session/node_modules/cookie/index.js
new file mode 100644
index 0000000..acd5acd
--- /dev/null
+++ b/node_modules/express-session/node_modules/cookie/index.js
@@ -0,0 +1,335 @@
+/*!
+ * cookie
+ * Copyright(c) 2012-2014 Roman Shtylman
+ * Copyright(c) 2015 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+'use strict';
+
+/**
+ * Module exports.
+ * @public
+ */
+
+exports.parse = parse;
+exports.serialize = serialize;
+
+/**
+ * Module variables.
+ * @private
+ */
+
+var __toString = Object.prototype.toString
+var __hasOwnProperty = Object.prototype.hasOwnProperty
+
+/**
+ * RegExp to match cookie-name in RFC 6265 sec 4.1.1
+ * This refers out to the obsoleted definition of token in RFC 2616 sec 2.2
+ * which has been replaced by the token definition in RFC 7230 appendix B.
+ *
+ * cookie-name       = token
+ * token             = 1*tchar
+ * tchar             = "!" / "#" / "$" / "%" / "&" / "'" /
+ *                     "*" / "+" / "-" / "." / "^" / "_" /
+ *                     "`" / "|" / "~" / DIGIT / ALPHA
+ */
+
+var cookieNameRegExp = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
+
+/**
+ * RegExp to match cookie-value in RFC 6265 sec 4.1.1
+ *
+ * cookie-value      = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
+ * cookie-octet      = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
+ *                     ; US-ASCII characters excluding CTLs,
+ *                     ; whitespace DQUOTE, comma, semicolon,
+ *                     ; and backslash
+ */
+
+var cookieValueRegExp = /^("?)[\u0021\u0023-\u002B\u002D-\u003A\u003C-\u005B\u005D-\u007E]*\1$/;
+
+/**
+ * RegExp to match domain-value in RFC 6265 sec 4.1.1
+ *
+ * domain-value      = <subdomain>
+ *                     ; defined in [RFC1034], Section 3.5, as
+ *                     ; enhanced by [RFC1123], Section 2.1
+ * <subdomain>       = <label> | <subdomain> "." <label>
+ * <label>           = <let-dig> [ [ <ldh-str> ] <let-dig> ]
+ *                     Labels must be 63 characters or less.
+ *                     'let-dig' not 'letter' in the first char, per RFC1123
+ * <ldh-str>         = <let-dig-hyp> | <let-dig-hyp> <ldh-str>
+ * <let-dig-hyp>     = <let-dig> | "-"
+ * <let-dig>         = <letter> | <digit>
+ * <letter>          = any one of the 52 alphabetic characters A through Z in
+ *                     upper case and a through z in lower case
+ * <digit>           = any one of the ten digits 0 through 9
+ *
+ * Keep support for leading dot: https://github.com/jshttp/cookie/issues/173
+ *
+ * > (Note that a leading %x2E ("."), if present, is ignored even though that
+ * character is not permitted, but a trailing %x2E ("."), if present, will
+ * cause the user agent to ignore the attribute.)
+ */
+
+var domainValueRegExp = /^([.]?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)([.][a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i;
+
+/**
+ * RegExp to match path-value in RFC 6265 sec 4.1.1
+ *
+ * path-value        = <any CHAR except CTLs or ";">
+ * CHAR              = %x01-7F
+ *                     ; defined in RFC 5234 appendix B.1
+ */
+
+var pathValueRegExp = /^[\u0020-\u003A\u003D-\u007E]*$/;
+
+/**
+ * Parse a cookie header.
+ *
+ * Parse the given cookie header string into an object
+ * The object has the various cookies as keys(names) => values
+ *
+ * @param {string} str
+ * @param {object} [opt]
+ * @return {object}
+ * @public
+ */
+
+function parse(str, opt) {
+  if (typeof str !== 'string') {
+    throw new TypeError('argument str must be a string');
+  }
+
+  var obj = {};
+  var len = str.length;
+  // RFC 6265 sec 4.1.1, RFC 2616 2.2 defines a cookie name consists of one char minimum, plus '='.
+  if (len < 2) return obj;
+
+  var dec = (opt && opt.decode) || decode;
+  var index = 0;
+  var eqIdx = 0;
+  var endIdx = 0;
+
+  do {
+    eqIdx = str.indexOf('=', index);
+    if (eqIdx === -1) break; // No more cookie pairs.
+
+    endIdx = str.indexOf(';', index);
+
+    if (endIdx === -1) {
+      endIdx = len;
+    } else if (eqIdx > endIdx) {
+      // backtrack on prior semicolon
+      index = str.lastIndexOf(';', eqIdx - 1) + 1;
+      continue;
+    }
+
+    var keyStartIdx = startIndex(str, index, eqIdx);
+    var keyEndIdx = endIndex(str, eqIdx, keyStartIdx);
+    var key = str.slice(keyStartIdx, keyEndIdx);
+
+    // only assign once
+    if (!__hasOwnProperty.call(obj, key)) {
+      var valStartIdx = startIndex(str, eqIdx + 1, endIdx);
+      var valEndIdx = endIndex(str, endIdx, valStartIdx);
+
+      if (str.charCodeAt(valStartIdx) === 0x22 /* " */ && str.charCodeAt(valEndIdx - 1) === 0x22 /* " */) {
+        valStartIdx++;
+        valEndIdx--;
+      }
+
+      var val = str.slice(valStartIdx, valEndIdx);
+      obj[key] = tryDecode(val, dec);
+    }
+
+    index = endIdx + 1
+  } while (index < len);
+
+  return obj;
+}
+
+function startIndex(str, index, max) {
+  do {
+    var code = str.charCodeAt(index);
+    if (code !== 0x20 /*   */ && code !== 0x09 /* \t */) return index;
+  } while (++index < max);
+  return max;
+}
+
+function endIndex(str, index, min) {
+  while (index > min) {
+    var code = str.charCodeAt(--index);
+    if (code !== 0x20 /*   */ && code !== 0x09 /* \t */) return index + 1;
+  }
+  return min;
+}
+
+/**
+ * Serialize data into a cookie header.
+ *
+ * Serialize a name value pair into a cookie string suitable for
+ * http headers. An optional options object specifies cookie parameters.
+ *
+ * serialize('foo', 'bar', { httpOnly: true })
+ *   => "foo=bar; httpOnly"
+ *
+ * @param {string} name
+ * @param {string} val
+ * @param {object} [opt]
+ * @return {string}
+ * @public
+ */
+
+function serialize(name, val, opt) {
+  var enc = (opt && opt.encode) || encodeURIComponent;
+
+  if (typeof enc !== 'function') {
+    throw new TypeError('option encode is invalid');
+  }
+
+  if (!cookieNameRegExp.test(name)) {
+    throw new TypeError('argument name is invalid');
+  }
+
+  var value = enc(val);
+
+  if (!cookieValueRegExp.test(value)) {
+    throw new TypeError('argument val is invalid');
+  }
+
+  var str = name + '=' + value;
+  if (!opt) return str;
+
+  if (null != opt.maxAge) {
+    var maxAge = Math.floor(opt.maxAge);
+
+    if (!isFinite(maxAge)) {
+      throw new TypeError('option maxAge is invalid')
+    }
+
+    str += '; Max-Age=' + maxAge;
+  }
+
+  if (opt.domain) {
+    if (!domainValueRegExp.test(opt.domain)) {
+      throw new TypeError('option domain is invalid');
+    }
+
+    str += '; Domain=' + opt.domain;
+  }
+
+  if (opt.path) {
+    if (!pathValueRegExp.test(opt.path)) {
+      throw new TypeError('option path is invalid');
+    }
+
+    str += '; Path=' + opt.path;
+  }
+
+  if (opt.expires) {
+    var expires = opt.expires
+
+    if (!isDate(expires) || isNaN(expires.valueOf())) {
+      throw new TypeError('option expires is invalid');
+    }
+
+    str += '; Expires=' + expires.toUTCString()
+  }
+
+  if (opt.httpOnly) {
+    str += '; HttpOnly';
+  }
+
+  if (opt.secure) {
+    str += '; Secure';
+  }
+
+  if (opt.partitioned) {
+    str += '; Partitioned'
+  }
+
+  if (opt.priority) {
+    var priority = typeof opt.priority === 'string'
+      ? opt.priority.toLowerCase() : opt.priority;
+
+    switch (priority) {
+      case 'low':
+        str += '; Priority=Low'
+        break
+      case 'medium':
+        str += '; Priority=Medium'
+        break
+      case 'high':
+        str += '; Priority=High'
+        break
+      default:
+        throw new TypeError('option priority is invalid')
+    }
+  }
+
+  if (opt.sameSite) {
+    var sameSite = typeof opt.sameSite === 'string'
+      ? opt.sameSite.toLowerCase() : opt.sameSite;
+
+    switch (sameSite) {
+      case true:
+        str += '; SameSite=Strict';
+        break;
+      case 'lax':
+        str += '; SameSite=Lax';
+        break;
+      case 'strict':
+        str += '; SameSite=Strict';
+        break;
+      case 'none':
+        str += '; SameSite=None';
+        break;
+      default:
+        throw new TypeError('option sameSite is invalid');
+    }
+  }
+
+  return str;
+}
+
+/**
+ * URL-decode string value. Optimized to skip native call when no %.
+ *
+ * @param {string} str
+ * @returns {string}
+ */
+
+function decode (str) {
+  return str.indexOf('%') !== -1
+    ? decodeURIComponent(str)
+    : str
+}
+
+/**
+ * Determine if value is a Date.
+ *
+ * @param {*} val
+ * @private
+ */
+
+function isDate (val) {
+  return __toString.call(val) === '[object Date]';
+}
+
+/**
+ * Try decoding a string using a decoding function.
+ *
+ * @param {string} str
+ * @param {function} decode
+ * @private
+ */
+
+function tryDecode(str, decode) {
+  try {
+    return decode(str);
+  } catch (e) {
+    return str;
+  }
+}
diff --git a/node_modules/express-session/node_modules/cookie/package.json b/node_modules/express-session/node_modules/cookie/package.json
new file mode 100644
index 0000000..22e3f92
--- /dev/null
+++ b/node_modules/express-session/node_modules/cookie/package.json
@@ -0,0 +1,44 @@
+{
+  "name": "cookie",
+  "description": "HTTP server cookie parsing and serialization",
+  "version": "0.7.2",
+  "author": "Roman Shtylman <shtylman@gmail.com>",
+  "contributors": [
+    "Douglas Christopher Wilson <doug@somethingdoug.com>"
+  ],
+  "license": "MIT",
+  "keywords": [
+    "cookie",
+    "cookies"
+  ],
+  "repository": "jshttp/cookie",
+  "devDependencies": {
+    "beautify-benchmark": "0.2.4",
+    "benchmark": "2.1.4",
+    "eslint": "8.53.0",
+    "eslint-plugin-markdown": "3.0.1",
+    "mocha": "10.2.0",
+    "nyc": "15.1.0",
+    "safe-buffer": "5.2.1",
+    "top-sites": "1.1.194"
+  },
+  "files": [
+    "HISTORY.md",
+    "LICENSE",
+    "README.md",
+    "SECURITY.md",
+    "index.js"
+  ],
+  "main": "index.js",
+  "engines": {
+    "node": ">= 0.6"
+  },
+  "scripts": {
+    "bench": "node benchmark/index.js",
+    "lint": "eslint .",
+    "test": "mocha --reporter spec --bail --check-leaks test/",
+    "test-ci": "nyc --reporter=lcov --reporter=text npm test",
+    "test-cov": "nyc --reporter=html --reporter=text npm test",
+    "update-bench": "node scripts/update-benchmark.js"
+  }
+}
diff --git a/node_modules/express-session/package.json b/node_modules/express-session/package.json
new file mode 100644
index 0000000..e332243
--- /dev/null
+++ b/node_modules/express-session/package.json
@@ -0,0 +1,47 @@
+{
+  "name": "express-session",
+  "version": "1.18.1",
+  "description": "Simple session middleware for Express",
+  "author": "TJ Holowaychuk <tj@vision-media.ca> (http://tjholowaychuk.com)",
+  "contributors": [
+    "Douglas Christopher Wilson <doug@somethingdoug.com>",
+    "Joe Wagner <njwjs722@gmail.com>"
+  ],
+  "repository": "expressjs/session",
+  "license": "MIT",
+  "dependencies": {
+    "cookie": "0.7.2",
+    "cookie-signature": "1.0.7",
+    "debug": "2.6.9",
+    "depd": "~2.0.0",
+    "on-headers": "~1.0.2",
+    "parseurl": "~1.3.3",
+    "safe-buffer": "5.2.1",
+    "uid-safe": "~2.1.5"
+  },
+  "devDependencies": {
+    "after": "0.8.2",
+    "cookie-parser": "1.4.6",
+    "eslint": "8.56.0",
+    "eslint-plugin-markdown": "3.0.1",
+    "express": "4.17.3",
+    "mocha": "10.2.0",
+    "nyc": "15.1.0",
+    "supertest": "6.3.4"
+  },
+  "files": [
+    "session/",
+    "HISTORY.md",
+    "index.js"
+  ],
+  "engines": {
+    "node": ">= 0.8.0"
+  },
+  "scripts": {
+    "lint": "eslint . && node ./scripts/lint-readme.js",
+    "test": "mocha --require test/support/env --check-leaks --bail --no-exit --reporter spec test/",
+    "test-ci": "nyc --reporter=lcov --reporter=text npm test",
+    "test-cov": "nyc npm test",
+    "version": "node scripts/version-history.js && git add HISTORY.md"
+  }
+}
diff --git a/node_modules/express-session/session/cookie.js b/node_modules/express-session/session/cookie.js
new file mode 100644
index 0000000..8bb5907
--- /dev/null
+++ b/node_modules/express-session/session/cookie.js
@@ -0,0 +1,152 @@
+/*!
+ * Connect - session - Cookie
+ * Copyright(c) 2010 Sencha Inc.
+ * Copyright(c) 2011 TJ Holowaychuk
+ * MIT Licensed
+ */
+
+'use strict';
+
+/**
+ * Module dependencies.
+ */
+
+var cookie = require('cookie')
+var deprecate = require('depd')('express-session')
+
+/**
+ * Initialize a new `Cookie` with the given `options`.
+ *
+ * @param {IncomingMessage} req
+ * @param {Object} options
+ * @api private
+ */
+
+var Cookie = module.exports = function Cookie(options) {
+  this.path = '/';
+  this.maxAge = null;
+  this.httpOnly = true;
+
+  if (options) {
+    if (typeof options !== 'object') {
+      throw new TypeError('argument options must be a object')
+    }
+
+    for (var key in options) {
+      if (key !== 'data') {
+        this[key] = options[key]
+      }
+    }
+  }
+
+  if (this.originalMaxAge === undefined || this.originalMaxAge === null) {
+    this.originalMaxAge = this.maxAge
+  }
+};
+
+/*!
+ * Prototype.
+ */
+
+Cookie.prototype = {
+
+  /**
+   * Set expires `date`.
+   *
+   * @param {Date} date
+   * @api public
+   */
+
+  set expires(date) {
+    this._expires = date;
+    this.originalMaxAge = this.maxAge;
+  },
+
+  /**
+   * Get expires `date`.
+   *
+   * @return {Date}
+   * @api public
+   */
+
+  get expires() {
+    return this._expires;
+  },
+
+  /**
+   * Set expires via max-age in `ms`.
+   *
+   * @param {Number} ms
+   * @api public
+   */
+
+  set maxAge(ms) {
+    if (ms && typeof ms !== 'number' && !(ms instanceof Date)) {
+      throw new TypeError('maxAge must be a number or Date')
+    }
+
+    if (ms instanceof Date) {
+      deprecate('maxAge as Date; pass number of milliseconds instead')
+    }
+
+    this.expires = typeof ms === 'number'
+      ? new Date(Date.now() + ms)
+      : ms;
+  },
+
+  /**
+   * Get expires max-age in `ms`.
+   *
+   * @return {Number}
+   * @api public
+   */
+
+  get maxAge() {
+    return this.expires instanceof Date
+      ? this.expires.valueOf() - Date.now()
+      : this.expires;
+  },
+
+  /**
+   * Return cookie data object.
+   *
+   * @return {Object}
+   * @api private
+   */
+
+  get data() {
+    return {
+      originalMaxAge: this.originalMaxAge,
+      partitioned: this.partitioned,
+      priority: this.priority
+      , expires: this._expires
+      , secure: this.secure
+      , httpOnly: this.httpOnly
+      , domain: this.domain
+      , path: this.path
+      , sameSite: this.sameSite
+    }
+  },
+
+  /**
+   * Return a serialized cookie string.
+   *
+   * @return {String}
+   * @api public
+   */
+
+  serialize: function(name, val){
+    return cookie.serialize(name, val, this.data);
+  },
+
+  /**
+   * Return JSON representation of this cookie.
+   *
+   * @return {Object}
+   * @api private
+   */
+
+  toJSON: function(){
+    return this.data;
+  }
+};
diff --git a/node_modules/express-session/session/memory.js b/node_modules/express-session/session/memory.js
new file mode 100644
index 0000000..11ed686
--- /dev/null
+++ b/node_modules/express-session/session/memory.js
@@ -0,0 +1,187 @@
+/*!
+ * express-session
+ * Copyright(c) 2010 Sencha Inc.
+ * Copyright(c) 2011 TJ Holowaychuk
+ * Copyright(c) 2015 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+'use strict';
+
+/**
+ * Module dependencies.
+ * @private
+ */
+
+var Store = require('./store')
+var util = require('util')
+
+/**
+ * Shim setImmediate for node.js < 0.10
+ * @private
+ */
+
+/* istanbul ignore next */
+var defer = typeof setImmediate === 'function'
+  ? setImmediate
+  : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) }
+
+/**
+ * Module exports.
+ */
+
+module.exports = MemoryStore
+
+/**
+ * A session store in memory.
+ * @public
+ */
+
+function MemoryStore() {
+  Store.call(this)
+  this.sessions = Object.create(null)
+}
+
+/**
+ * Inherit from Store.
+ */
+
+util.inherits(MemoryStore, Store)
+
+/**
+ * Get all active sessions.
+ *
+ * @param {function} callback
+ * @public
+ */
+
+MemoryStore.prototype.all = function all(callback) {
+  var sessionIds = Object.keys(this.sessions)
+  var sessions = Object.create(null)
+
+  for (var i = 0; i < sessionIds.length; i++) {
+    var sessionId = sessionIds[i]
+    var session = getSession.call(this, sessionId)
+
+    if (session) {
+      sessions[sessionId] = session;
+    }
+  }
+
+  callback && defer(callback, null, sessions)
+}
+
+/**
+ * Clear all sessions.
+ *
+ * @param {function} callback
+ * @public
+ */
+
+MemoryStore.prototype.clear = function clear(callback) {
+  this.sessions = Object.create(null)
+  callback && defer(callback)
+}
+
+/**
+ * Destroy the session associated with the given session ID.
+ *
+ * @param {string} sessionId
+ * @public
+ */
+
+MemoryStore.prototype.destroy = function destroy(sessionId, callback) {
+  delete this.sessions[sessionId]
+  callback && defer(callback)
+}
+
+/**
+ * Fetch session by the given session ID.
+ *
+ * @param {string} sessionId
+ * @param {function} callback
+ * @public
+ */
+
+MemoryStore.prototype.get = function get(sessionId, callback) {
+  defer(callback, null, getSession.call(this, sessionId))
+}
+
+/**
+ * Commit the given session associated with the given sessionId to the store.
+ *
+ * @param {string} sessionId
+ * @param {object} session
+ * @param {function} callback
+ * @public
+ */
+
+MemoryStore.prototype.set = function set(sessionId, session, callback) {
+  this.sessions[sessionId] = JSON.stringify(session)
+  callback && defer(callback)
+}
+
+/**
+ * Get number of active sessions.
+ *
+ * @param {function} callback
+ * @public
+ */
+
+MemoryStore.prototype.length = function length(callback) {
+  this.all(function (err, sessions) {
+    if (err) return callback(err)
+    callback(null, Object.keys(sessions).length)
+  })
+}
+
+/**
+ * Touch the given session object associated with the given session ID.
+ *
+ * @param {string} sessionId
+ * @param {object} session
+ * @param {function} callback
+ * @public
+ */
+
+MemoryStore.prototype.touch = function touch(sessionId, session, callback) {
+  var currentSession = getSession.call(this, sessionId)
+
+  if (currentSession) {
+    // update expiration
+    currentSession.cookie = session.cookie
+    this.sessions[sessionId] = JSON.stringify(currentSession)
+  }
+
+  callback && defer(callback)
+}
+
+/**
+ * Get session from the store.
+ * @private
+ */
+
+function getSession(sessionId) {
+  var sess = this.sessions[sessionId]
+
+  if (!sess) {
+    return
+  }
+
+  // parse
+  sess = JSON.parse(sess)
+
+  if (sess.cookie) {
+    var expires = typeof sess.cookie.expires === 'string'
+      ? new Date(sess.cookie.expires)
+      : sess.cookie.expires
+
+    // destroy expired session
+    if (expires && expires <= Date.now()) {
+      delete this.sessions[sessionId]
+      return
+    }
+  }
+
+  return sess
+}
diff --git a/node_modules/express-session/session/session.js b/node_modules/express-session/session/session.js
new file mode 100644
index 0000000..fee7608
--- /dev/null
+++ b/node_modules/express-session/session/session.js
@@ -0,0 +1,143 @@
+/*!
+ * Connect - session - Session
+ * Copyright(c) 2010 Sencha Inc.
+ * Copyright(c) 2011 TJ Holowaychuk
+ * MIT Licensed
+ */
+
+'use strict';
+
+/**
+ * Expose Session.
+ */
+
+module.exports = Session;
+
+/**
+ * Create a new `Session` with the given request and `data`.
+ *
+ * @param {IncomingRequest} req
+ * @param {Object} data
+ * @api private
+ */
+
+function Session(req, data) {
+  Object.defineProperty(this, 'req', { value: req });
+  Object.defineProperty(this, 'id', { value: req.sessionID });
+
+  if (typeof data === 'object' && data !== null) {
+    // merge data into this, ignoring prototype properties
+    for (var prop in data) {
+      if (!(prop in this)) {
+        this[prop] = data[prop]
+      }
+    }
+  }
+}
+
+/**
+ * Update reset `.cookie.maxAge` to prevent
+ * the cookie from expiring when the
+ * session is still active.
+ *
+ * @return {Session} for chaining
+ * @api public
+ */
+
+defineMethod(Session.prototype, 'touch', function touch() {
+  return this.resetMaxAge();
+});
+
+/**
+ * Reset `.maxAge` to `.originalMaxAge`.
+ *
+ * @return {Session} for chaining
+ * @api public
+ */
+
+defineMethod(Session.prototype, 'resetMaxAge', function resetMaxAge() {
+  this.cookie.maxAge = this.cookie.originalMaxAge;
+  return this;
+});
+
+/**
+ * Save the session data with optional callback `fn(err)`.
+ *
+ * @param {Function} fn
+ * @return {Session} for chaining
+ * @api public
+ */
+
+defineMethod(Session.prototype, 'save', function save(fn) {
+  this.req.sessionStore.set(this.id, this, fn || function(){});
+  return this;
+});
+
+/**
+ * Re-loads the session data _without_ altering
+ * the maxAge properties. Invokes the callback `fn(err)`,
+ * after which time if no exception has occurred the
+ * `req.session` property will be a new `Session` object,
+ * although representing the same session.
+ *
+ * @param {Function} fn
+ * @return {Session} for chaining
+ * @api public
+ */
+
+defineMethod(Session.prototype, 'reload', function reload(fn) {
+  var req = this.req
+  var store = this.req.sessionStore
+
+  store.get(this.id, function(err, sess){
+    if (err) return fn(err);
+    if (!sess) return fn(new Error('failed to load session'));
+    store.createSession(req, sess);
+    fn();
+  });
+  return this;
+});
+
+/**
+ * Destroy `this` session.
+ *
+ * @param {Function} fn
+ * @return {Session} for chaining
+ * @api public
+ */
+
+defineMethod(Session.prototype, 'destroy', function destroy(fn) {
+  delete this.req.session;
+  this.req.sessionStore.destroy(this.id, fn);
+  return this;
+});
+
+/**
+ * Regenerate this request's session.
+ *
+ * @param {Function} fn
+ * @return {Session} for chaining
+ * @api public
+ */
+
+defineMethod(Session.prototype, 'regenerate', function regenerate(fn) {
+  this.req.sessionStore.regenerate(this.req, fn);
+  return this;
+});
+
+/**
+ * Helper function for creating a method on a prototype.
+ *
+ * @param {Object} obj
+ * @param {String} name
+ * @param {Function} fn
+ * @private
+ */
+function defineMethod(obj, name, fn) {
+  Object.defineProperty(obj, name, {
+    configurable: true,
+    enumerable: false,
+    value: fn,
+    writable: true
+  });
+};
diff --git a/node_modules/express-session/session/store.js b/node_modules/express-session/session/store.js
new file mode 100644
index 0000000..3793877
--- /dev/null
+++ b/node_modules/express-session/session/store.js
@@ -0,0 +1,102 @@
+/*!
+ * Connect - session - Store
+ * Copyright(c) 2010 Sencha Inc.
+ * Copyright(c) 2011 TJ Holowaychuk
+ * MIT Licensed
+ */
+
+'use strict';
+
+/**
+ * Module dependencies.
+ * @private
+ */
+
+var Cookie = require('./cookie')
+var EventEmitter = require('events').EventEmitter
+var Session = require('./session')
+var util = require('util')
+
+/**
+ * Module exports.
+ * @public
+ */
+
+module.exports = Store
+
+/**
+ * Abstract base class for session stores.
+ * @public
+ */
+
+function Store () {
+  EventEmitter.call(this)
+}
+
+/**
+ * Inherit from EventEmitter.
+ */
+
+util.inherits(Store, EventEmitter)
+
+/**
+ * Re-generate the given requests's session.
+ *
+ * @param {IncomingRequest} req
+ * @return {Function} fn
+ * @api public
+ */
+
+Store.prototype.regenerate = function(req, fn){
+  var self = this;
+  this.destroy(req.sessionID, function(err){
+    self.generate(req);
+    fn(err);
+  });
+};
+
+/**
+ * Load a `Session` instance via the given `sid`
+ * and invoke the callback `fn(err, sess)`.
+ *
+ * @param {String} sid
+ * @param {Function} fn
+ * @api public
+ */
+
+Store.prototype.load = function(sid, fn){
+  var self = this;
+  this.get(sid, function(err, sess){
+    if (err) return fn(err);
+    if (!sess) return fn();
+    var req = { sessionID: sid, sessionStore: self };
+    fn(null, self.createSession(req, sess))
+  });
+};
+
+/**
+ * Create session from JSON `sess` data.
+ *
+ * @param {IncomingRequest} req
+ * @param {Object} sess
+ * @return {Session}
+ * @api private
+ */
+
+Store.prototype.createSession = function(req, sess){
+  var expires = sess.cookie.expires
+  var originalMaxAge = sess.cookie.originalMaxAge
+
+  sess.cookie = new Cookie(sess.cookie);
+
+  if (typeof expires === 'string') {
+    // convert expires to a Date object
+    sess.cookie.expires = new Date(expires)
+  }
+
+  // keep originalMaxAge intact
+  sess.cookie.originalMaxAge = originalMaxAge
+
+  req.session = new Session(req, sess);
+  return req.session;
+};
diff --git a/node_modules/on-headers/index.js b/node_modules/on-headers/index.js
new file mode 100644
index 0000000..7db6375
--- /dev/null
+++ b/node_modules/on-headers/index.js
@@ -0,0 +1,132 @@
+/*!
+ * on-headers
+ * Copyright(c) 2014 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+'use strict'
+
+/**
+ * Module exports.
+ * @public
+ */
+
+module.exports = onHeaders
+
+/**
+ * Create a replacement writeHead method.
+ *
+ * @param {function} prevWriteHead
+ * @param {function} listener
+ * @private
+ */
+
+function createWriteHead (prevWriteHead, listener) {
+  var fired = false
+
+  // return function with core name and argument list
+  return function writeHead (statusCode) {
+    // set headers from arguments
+    var args = setWriteHeadHeaders.apply(this, arguments)
+
+    // fire listener
+    if (!fired) {
+      fired = true
+      listener.call(this)
+
+      // pass-along an updated status code
+      if (typeof args[0] === 'number' && this.statusCode !== args[0]) {
+        args[0] = this.statusCode
+        args.length = 1
+      }
+    }
+
+    return prevWriteHead.apply(this, args)
+  }
+}
+
+/**
+ * Execute a listener when a response is about to write headers.
+ *
+ * @param {object} res
+ * @return {function} listener
+ * @public
+ */
+
+function onHeaders (res, listener) {
+  if (!res) {
+    throw new TypeError('argument res is required')
+  }
+
+  if (typeof listener !== 'function') {
+    throw new TypeError('argument listener must be a function')
+  }
+
+  res.writeHead = createWriteHead(res.writeHead, listener)
+}
+
+/**
+ * Set headers contained in array on the response object.
+ *
+ * @param {object} res
+ * @param {array} headers
+ * @private
+ */
+
+function setHeadersFromArray (res, headers) {
+  for (var i = 0; i < headers.length; i++) {
+    res.setHeader(headers[i][0], headers[i][1])
+  }
+}
+
+/**
+ * Set headers contained in object on the response object.
+ *
+ * @param {object} res
+ * @param {object} headers
+ * @private
+ */
+
+function setHeadersFromObject (res, headers) {
+  var keys = Object.keys(headers)
+  for (var i = 0; i < keys.length; i++) {
+    var k = keys[i]
+    if (k) res.setHeader(k, headers[k])
+  }
+}
+
+/**
+ * Set headers and other properties on the response object.
+ *
+ * @param {number} statusCode
+ * @private
+ */
+
+function setWriteHeadHeaders (statusCode) {
+  var length = arguments.length
+  var headerIndex = length > 1 && typeof arguments[1] === 'string'
+    ? 2
+    : 1
+
+  var headers = length >= headerIndex + 1
+    ? arguments[headerIndex]
+    : undefined
+
+  this.statusCode = statusCode
+
+  if (Array.isArray(headers)) {
+    // handle array case
+    setHeadersFromArray(this, headers)
+  } else if (headers) {
+    // handle object case
+    setHeadersFromObject(this, headers)
+  }
+
+  // copy leading arguments
+  var args = new Array(Math.min(length, headerIndex))
+  for (var i = 0; i < args.length; i++) {
+    args[i] = arguments[i]
+  }
+
+  return args
+}
diff --git a/node_modules/on-headers/package.json b/node_modules/on-headers/package.json
new file mode 100644
index 0000000..1e9bf9e
--- /dev/null
+++ b/node_modules/on-headers/package.json
@@ -0,0 +1,42 @@
+{
+  "name": "on-headers",
+  "description": "Execute a listener when a response is about to write headers",
+  "version": "1.0.2",
+  "author": "Douglas Christopher Wilson <doug@somethingdoug.com>",
+  "license": "MIT",
+  "keywords": [
+    "event",
+    "headers",
+    "http",
+    "onheaders"
+  ],
+  "repository": "jshttp/on-headers",
+  "devDependencies": {
+    "eslint": "5.14.1",
+    "eslint-config-standard": "12.0.0",
+    "eslint-plugin-import": "2.16.0",
+    "eslint-plugin-markdown": "1.0.0",
+    "eslint-plugin-node": "8.0.1",
+    "eslint-plugin-promise": "4.0.1",
+    "eslint-plugin-standard": "4.0.0",
+    "istanbul": "0.4.5",
+    "mocha": "6.0.1",
+    "supertest": "3.4.2"
+  },
+  "files": [
+    "LICENSE",
+    "HISTORY.md",
+    "README.md",
+    "index.js"
+  ],
+  "engines": {
+    "node": ">= 0.8"
+  },
+  "scripts": {
+    "lint": "eslint --plugin markdown --ext js,md .",
+    "test": "mocha --reporter spec --bail --check-leaks test/",
+    "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot --check-leaks test/",
+    "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec --check-leaks test/",
+    "version": "node scripts/version-history.js && git add HISTORY.md"
+  }
+}
diff --git a/node_modules/random-bytes/index.js b/node_modules/random-bytes/index.js
new file mode 100644
index 0000000..9ad930f
--- /dev/null
+++ b/node_modules/random-bytes/index.js
@@ -0,0 +1,101 @@
+/*!
+ * random-bytes
+ * Copyright(c) 2016 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+'use strict'
+
+/**
+ * Module dependencies.
+ * @private
+ */
+
+var crypto = require('crypto')
+
+/**
+ * Module variables.
+ * @private
+ */
+
+var generateAttempts = crypto.randomBytes === crypto.pseudoRandomBytes ? 1 : 3
+
+/**
+ * Module exports.
+ * @public
+ */
+
+module.exports = randomBytes
+module.exports.sync = randomBytesSync
+
+/**
+ * Generates strong pseudo-random bytes.
+ *
+ * @param {number} size
+ * @param {function} [callback]
+ * @return {Promise}
+ * @public
+ */
+
+function randomBytes(size, callback) {
+  // validate callback is a function, if provided
+  if (callback !== undefined && typeof callback !== 'function') {
+    throw new TypeError('argument callback must be a function')
+  }
+
+  // require the callback without promises
+  if (!callback && !global.Promise) {
+    throw new TypeError('argument callback is required')
+  }
+
+  if (callback) {
+    // classic callback style
+    return generateRandomBytes(size, generateAttempts, callback)
+  }
+
+  return new Promise(function executor(resolve, reject) {
+    generateRandomBytes(size, generateAttempts, function onRandomBytes(err, str) {
+      if (err) return reject(err)
+      resolve(str)
+    })
+  })
+}
+
+/**
+ * Generates strong pseudo-random bytes sync.
+ *
+ * @param {number} size
+ * @return {Buffer}
+ * @public
+ */
+
+function randomBytesSync(size) {
+  var err = null
+
+  for (var i = 0; i < generateAttempts; i++) {
+    try {
+      return crypto.randomBytes(size)
+    } catch (e) {
+      err = e
+    }
+  }
+
+  throw err
+}
+
+/**
+ * Generates strong pseudo-random bytes.
+ *
+ * @param {number} size
+ * @param {number} attempts
+ * @param {function} callback
+ * @private
+ */
+
+function generateRandomBytes(size, attempts, callback) {
+  crypto.randomBytes(size, function onRandomBytes(err, buf) {
+    if (!err) return callback(null, buf)
+    if (!--attempts) return callback(err)
+    setTimeout(generateRandomBytes.bind(null, size, attempts, callback), 10)
+  })
+}
diff --git a/node_modules/random-bytes/package.json b/node_modules/random-bytes/package.json
new file mode 100644
index 0000000..c67e0e8
--- /dev/null
+++ b/node_modules/random-bytes/package.json
@@ -0,0 +1,36 @@
+{
+  "name": "random-bytes",
+  "description": "URL and cookie safe UIDs",
+  "version": "1.0.0",
+  "contributors": [
+    "Douglas Christopher Wilson <doug@somethingdoug.com>"
+  ],
+  "license": "MIT",
+  "repository": "crypto-utils/random-bytes",
+  "devDependencies": {
+    "bluebird": "3.1.1",
+    "istanbul": "0.4.2",
+    "mocha": "2.3.4",
+    "proxyquire": "1.2.0"
+  },
+  "files": [
+    "LICENSE",
+    "HISTORY.md",
+    "README.md",
+    "index.js"
+  ],
+  "engines": {
+    "node": ">= 0.8"
+  },
+  "scripts": {
+    "test": "mocha --trace-deprecation --reporter spec --bail --check-leaks test/",
+    "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --trace-deprecation --reporter dot --check-leaks test/",
+    "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --trace-deprecation --reporter spec --check-leaks test/"
+  },
+  "keywords": [
+    "bytes",
+    "generator",
+    "random",
+    "safe"
+  ]
+}
diff --git a/node_modules/uid-safe/index.js b/node_modules/uid-safe/index.js
new file mode 100644
index 0000000..18e8492
--- /dev/null
+++ b/node_modules/uid-safe/index.js
@@ -0,0 +1,107 @@
+/*!
+ * uid-safe
+ * Copyright(c) 2014 Jonathan Ong
+ * Copyright(c) 2015-2017 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+'use strict'
+
+/**
+ * Module dependencies.
+ * @private
+ */
+
+var randomBytes = require('random-bytes')
+
+/**
+ * Module variables.
+ * @private
+ */
+
+var EQUAL_END_REGEXP = /=+$/
+var PLUS_GLOBAL_REGEXP = /\+/g
+var SLASH_GLOBAL_REGEXP = /\//g
+
+/**
+ * Module exports.
+ * @public
+ */
+
+module.exports = uid
+module.exports.sync = uidSync
+
+/**
+ * Create a unique ID.
+ *
+ * @param {number} length
+ * @param {function} [callback]
+ * @return {Promise}
+ * @public
+ */
+
+function uid (length, callback) {
+  // validate callback is a function, if provided
+  if (callback !== undefined && typeof callback !== 'function') {
+    throw new TypeError('argument callback must be a function')
+  }
+
+  // require the callback without promises
+  if (!callback && !global.Promise) {
+    throw new TypeError('argument callback is required')
+  }
+
+  if (callback) {
+    // classic callback style
+    return generateUid(length, callback)
+  }
+
+  return new Promise(function executor (resolve, reject) {
+    generateUid(length, function onUid (err, str) {
+      if (err) return reject(err)
+      resolve(str)
+    })
+  })
+}
+
+/**
+ * Create a unique ID sync.
+ *
+ * @param {number} length
+ * @return {string}
+ * @public
+ */
+
+function uidSync (length) {
+  return toString(randomBytes.sync(length))
+}
+
+/**
+ * Generate a unique ID string.
+ *
+ * @param {number} length
+ * @param {function} callback
+ * @private
+ */
+
+function generateUid (length, callback) {
+  randomBytes(length, function (err, buf) {
+    if (err) return callback(err)
+    callback(null, toString(buf))
+  })
+}
+
+/**
+ * Change a Buffer into a string.
+ *
+ * @param {Buffer} buf
+ * @return {string}
+ * @private
+ */
+
+function toString (buf) {
+  return buf.toString('base64')
+    .replace(EQUAL_END_REGEXP, '')
+    .replace(PLUS_GLOBAL_REGEXP, '-')
+    .replace(SLASH_GLOBAL_REGEXP, '_')
+}
diff --git a/node_modules/uid-safe/package.json b/node_modules/uid-safe/package.json
new file mode 100644
index 0000000..9d9ea4c
--- /dev/null
+++ b/node_modules/uid-safe/package.json
@@ -0,0 +1,46 @@
+{
+  "name": "uid-safe",
+  "description": "URL and cookie safe UIDs",
+  "version": "2.1.5",
+  "contributors": [
+    "Douglas Christopher Wilson <doug@somethingdoug.com>",
+    "Jonathan Ong <me@jongleberry.com> (http://jongleberry.com)"
+  ],
+  "license": "MIT",
+  "repository": "crypto-utils/uid-safe",
+  "dependencies": {
+    "random-bytes": "~1.0.0"
+  },
+  "devDependencies": {
+    "bluebird": "3.5.0",
+    "eslint": "3.19.0",
+    "eslint-config-standard": "10.2.1",
+    "eslint-plugin-import": "2.7.0",
+    "eslint-plugin-node": "5.1.1",
+    "eslint-plugin-promise": "3.5.0",
+    "eslint-plugin-standard": "3.0.1",
+    "istanbul": "0.4.5",
+    "mocha": "2.5.3"
+  },
+  "files": [
+    "LICENSE",
+    "HISTORY.md",
+    "README.md",
+    "index.js"
+  ],
+  "engines": {
+    "node": ">= 0.8"
+  },
+  "scripts": {
+    "lint": "eslint .",
+    "test": "mocha --trace-deprecation --reporter spec --bail --check-leaks test/",
+    "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --trace-deprecation --reporter dot --check-leaks test/",
+    "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --trace-deprecation --reporter spec --check-leaks test/"
+  },
+  "keywords": [
+    "random",
+    "generator",
+    "uid",
+    "safe"
+  ]
+}
diff --git a/package-lock.json b/package-lock.json
index 4cd596f..e3f6cda 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,8 +9,11 @@
       "version": "1.0.0",
       "dependencies": {
         "body-parser": "^2.2.0",
+        "call-of-duty-api": "^4.1.0",
         "csso": "^5.0.5",
         "express": "^4.21.2",
+        "express-rate-limit": "^7.5.0",
+        "express-session": "^1.18.1",
         "glob": "^11.0.1",
         "html-minifier": "^4.0.0",
         "pkg": "^5.8.1",
@@ -82,6 +85,15 @@
         "node": ">=6.9.0"
       }
     },
+    "node_modules/@fastify/busboy": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
+      "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=14"
+      }
+    },
     "node_modules/@isaacs/cliui": {
       "version": "8.0.2",
       "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -528,6 +540,33 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/call-of-duty-api": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/call-of-duty-api/-/call-of-duty-api-4.1.0.tgz",
+      "integrity": "sha512-f5DJ6gQru6f406QVBZkkXOv0gUzFu0hykdkyKRa2Am6iWwGRVzcBK7rq+xHpNI6oTq3EFfw0T70kb38rZaezNA==",
+      "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.4.0",
+        "undici": "^5.12.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/Lierrmm"
+      }
+    },
+    "node_modules/call-of-duty-api/node_modules/undici": {
+      "version": "5.29.0",
+      "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
+      "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==",
+      "license": "MIT",
+      "dependencies": {
+        "@fastify/busboy": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=14.0"
+      }
+    },
     "node_modules/camel-case": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz",
@@ -1007,6 +1046,55 @@
         "url": "https://opencollective.com/express"
       }
     },
+    "node_modules/express-rate-limit": {
+      "version": "7.5.0",
+      "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz",
+      "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/express-rate-limit"
+      },
+      "peerDependencies": {
+        "express": "^4.11 || 5 || ^5.0.0-beta.1"
+      }
+    },
+    "node_modules/express-session": {
+      "version": "1.18.1",
+      "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz",
+      "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==",
+      "license": "MIT",
+      "dependencies": {
+        "cookie": "0.7.2",
+        "cookie-signature": "1.0.7",
+        "debug": "2.6.9",
+        "depd": "~2.0.0",
+        "on-headers": "~1.0.2",
+        "parseurl": "~1.3.3",
+        "safe-buffer": "5.2.1",
+        "uid-safe": "~2.1.5"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/express-session/node_modules/cookie": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+      "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/express-session/node_modules/cookie-signature": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+      "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+      "license": "MIT"
+    },
     "node_modules/express/node_modules/body-parser": {
       "version": "1.20.3",
       "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
@@ -1889,6 +1977,15 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/on-headers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+      "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/once": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -2151,6 +2248,15 @@
       ],
       "license": "MIT"
     },
+    "node_modules/random-bytes": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
+      "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/range-parser": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -2884,6 +2990,18 @@
         "node": ">=0.8.0"
       }
     },
+    "node_modules/uid-safe": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
+      "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
+      "license": "MIT",
+      "dependencies": {
+        "random-bytes": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/undici": {
       "version": "7.6.0",
       "resolved": "https://registry.npmjs.org/undici/-/undici-7.6.0.tgz",
diff --git a/package.json b/package.json
index 87bc149..7491707 100644
--- a/package.json
+++ b/package.json
@@ -20,8 +20,11 @@
   },
   "dependencies": {
     "body-parser": "^2.2.0",
+    "call-of-duty-api": "^4.1.0",
     "csso": "^5.0.5",
     "express": "^4.21.2",
+    "express-rate-limit": "^7.5.0",
+    "express-session": "^1.18.1",
     "glob": "^11.0.1",
     "html-minifier": "^4.0.0",
     "pkg": "^5.8.1",
diff --git a/src/css/styles.css b/src/css/styles.css
index 7669571..153bdbc 100644
--- a/src/css/styles.css
+++ b/src/css/styles.css
@@ -228,6 +228,22 @@ button:hover {
   background-color: #d32f2f;
 }
 
+/* Demo mode styles */
+.demo-mode {
+  background-color: #f0f0f0;
+  color: #888;
+  cursor: not-allowed;
+}
+
+.demo-notice {
+  background-color: #f8f9fa;
+  border-left: 4px solid #007bff;
+  padding: 10px 15px;
+  margin-bottom: 20px;
+  border-radius: 4px;
+  color: #495057;
+}
+
 @keyframes octocat-wave {
   0%,
   100% {
diff --git a/src/js/backend-min.js b/src/js/backend-min.js
new file mode 100644
index 0000000..285e576
--- /dev/null
+++ b/src/js/backend-min.js
@@ -0,0 +1,381 @@
+const a0_0x3e5ceb = a0_0x1776;
+function a0_0x424e() {
+  const _0x308173 = [
+    '8HLbzTb',
+    'number',
+    'replace',
+    'endTime',
+    'timePlayed',
+    'uiAPI',
+    'GMT',
+    'forEach',
+    '[CLIENT]\x20Request\x20data:\x20',
+    'An\x20error\x20occurred',
+    '[CLIENT]\x20Response\x20received\x20at\x20',
+    '\x20Hours\x20',
+    'includes',
+    '\x20at\x20',
+    'getElementById',
+    'dateAdded',
+    'toISOString',
+    'textContent',
+    'Request\x20timed\x20out.\x20Please\x20try\x20again.',
+    'string',
+    'Fetch\x20error:',
+    'length',
+    '828318JvPrOY',
+    'time',
+    'replaceKeysOption',
+    'toString',
+    'substring',
+    'json',
+    'An\x20error\x20occurred\x20while\x20fetching\x20data.',
+    'keys',
+    'entries',
+    '274851lFAePH',
+    'displayError',
+    'toUTCString',
+    'checked',
+    '5mFBRcl',
+    '1809891DxJKoQ',
+    'DOMContentLoaded',
+    '7128aSefdV',
+    'Server\x20returned\x20non-JSON\x20response',
+    'none',
+    'log',
+    'stringify',
+    'currentData',
+    'object',
+    'displayResults',
+    'appState',
+    'headers',
+    'null',
+    'signal',
+    '\x20Days\x20',
+    'error',
+    'style',
+    'test',
+    '\x20Minutes\x20',
+    'utcStartSeconds',
+    'tutorialDismissed',
+    'isArray',
+    'trimStart',
+    'ssoToken',
+    'querySelectorAll',
+    '70rCMOkH',
+    'addEventListener',
+    '1218049tFLRcR',
+    '.tutorial',
+    'repeat',
+    '16176Hmtcab',
+    'message',
+    'loading',
+    '[CLIENT]\x20Response\x20status:\x20',
+    'map',
+    'POST',
+    'floor',
+    '70193EjRYyO',
+    'name',
+    'objTime',
+    'display',
+    'date',
+    'get',
+    'Error:\x20',
+    'content-type',
+    'application/json',
+    'status',
+    'UTC',
+    'boolean',
+    '2031464mgplJK',
+    '8ohsaRV',
+  ];
+  a0_0x424e = function () {
+    return _0x308173;
+  };
+  return a0_0x424e();
+}
+(function (_0x26b91f, _0x29f7ca) {
+  const _0x4906c0 = a0_0x1776,
+    _0x243b93 = _0x26b91f();
+  while (!![]) {
+    try {
+      const _0x1e80bd =
+        (parseInt(_0x4906c0(0x11e)) / 0x1) *
+          (-parseInt(_0x4906c0(0x12b)) / 0x2) +
+        -parseInt(_0x4906c0(0x150)) / 0x3 +
+        parseInt(_0x4906c0(0x12a)) / 0x4 +
+        (-parseInt(_0x4906c0(0x14f)) / 0x5) *
+          (-parseInt(_0x4906c0(0x142)) / 0x6) +
+        (parseInt(_0x4906c0(0x114)) / 0x7) *
+          (parseInt(_0x4906c0(0x12c)) / 0x8) +
+        (parseInt(_0x4906c0(0x14b)) / 0x9) *
+          (-parseInt(_0x4906c0(0x112)) / 0xa) +
+        (-parseInt(_0x4906c0(0x152)) / 0xb) *
+          (-parseInt(_0x4906c0(0x117)) / 0xc);
+      if (_0x1e80bd === _0x29f7ca) break;
+      else _0x243b93['push'](_0x243b93['shift']());
+    } catch (_0x14bf47) {
+      _0x243b93['push'](_0x243b93['shift']());
+    }
+  }
+})(a0_0x424e, 0x91684),
+  (window['backendAPI'] = {
+    fetchData: fetchData,
+    jsonToYAML: jsonToYAML,
+    formatDuration: formatDuration,
+    formatEpochTime: formatEpochTime,
+    processTimestamps: processTimestamps,
+  }),
+  (window['appState'] = {
+    currentData: null,
+    outputFormat: a0_0x3e5ceb(0x147),
+    tutorialDismissed: ![],
+  }),
+  document[a0_0x3e5ceb(0x113)](a0_0x3e5ceb(0x151), function () {});
+function jsonToYAML(_0x57e066) {
+  const _0x4e69bf = a0_0x3e5ceb,
+    _0x1e3ebf = 0x2;
+  function _0x58e6eb(_0x4d120a, _0x248fd6 = 0x0) {
+    const _0x3a284a = a0_0x1776,
+      _0x52d5f6 = '\x20'[_0x3a284a(0x116)](_0x248fd6);
+    if (_0x4d120a === null) return _0x3a284a(0x105);
+    if (_0x4d120a === undefined) return '';
+    if (typeof _0x4d120a === _0x3a284a(0x13f)) {
+      if (
+        /[:{}[\],&*#?|\-<>=!%@`]/[_0x3a284a(0x10a)](_0x4d120a) ||
+        _0x4d120a === '' ||
+        !isNaN(_0x4d120a)
+      )
+        return '\x22' + _0x4d120a[_0x3a284a(0x12e)](/"/g, '\x5c\x22') + '\x22';
+      return _0x4d120a;
+    }
+    if (
+      typeof _0x4d120a === _0x3a284a(0x12d) ||
+      typeof _0x4d120a === _0x3a284a(0x129)
+    )
+      return _0x4d120a[_0x3a284a(0x145)]();
+    if (Array[_0x3a284a(0x10e)](_0x4d120a)) {
+      if (_0x4d120a[_0x3a284a(0x141)] === 0x0) return '[]';
+      let _0x53132f = '';
+      for (const _0x194c43 of _0x4d120a) {
+        _0x53132f +=
+          '\x0a' +
+          _0x52d5f6 +
+          '-\x20' +
+          _0x58e6eb(_0x194c43, _0x248fd6 + _0x1e3ebf)[_0x3a284a(0x10f)]();
+      }
+      return _0x53132f;
+    }
+    if (typeof _0x4d120a === 'object') {
+      if (Object[_0x3a284a(0x149)](_0x4d120a)[_0x3a284a(0x141)] === 0x0)
+        return '{}';
+      let _0x42a784 = '';
+      for (const [_0x1a8665, _0x32613d] of Object[_0x3a284a(0x14a)](
+        _0x4d120a
+      )) {
+        const _0x3a78cc = _0x58e6eb(_0x32613d, _0x248fd6 + _0x1e3ebf);
+        _0x3a78cc[_0x3a284a(0x138)]('\x0a') ?
+          (_0x42a784 += '\x0a' + _0x52d5f6 + _0x1a8665 + ':' + _0x3a78cc)
+        : (_0x42a784 += '\x0a' + _0x52d5f6 + _0x1a8665 + ':\x20' + _0x3a78cc);
+      }
+      return _0x42a784;
+    }
+    return String(_0x4d120a);
+  }
+  return _0x58e6eb(_0x57e066, 0x0)[_0x4e69bf(0x146)](0x1);
+}
+async function fetchData(_0x5d7e74, _0x3de528) {
+  const _0x5f0540 = a0_0x3e5ceb;
+  console['log'](
+    '[CLIENT]\x20Request\x20to\x20' +
+      _0x5d7e74 +
+      _0x5f0540(0x139) +
+      new Date()[_0x5f0540(0x13c)]()
+  ),
+    console['log'](
+      _0x5f0540(0x134) +
+        JSON[_0x5f0540(0x156)]({
+          ..._0x3de528,
+          ssoToken:
+            _0x3de528[_0x5f0540(0x110)] ?
+              _0x3de528['ssoToken'][_0x5f0540(0x146)](0x0, 0x5) + '...'
+            : _0x5f0540(0x154),
+        })
+    );
+  const _0x4d0c9e = document['getElementById']('error'),
+    _0x4a22b8 = document['getElementById'](_0x5f0540(0x119)),
+    _0x2f0994 = document[_0x5f0540(0x13a)]('results');
+  (_0x4d0c9e[_0x5f0540(0x13d)] = ''),
+    (_0x2f0994[_0x5f0540(0x109)][_0x5f0540(0x121)] = _0x5f0540(0x154)),
+    (_0x4a22b8[_0x5f0540(0x109)][_0x5f0540(0x121)] = 'block');
+  !window[_0x5f0540(0x103)][_0x5f0540(0x10d)] &&
+    ((window[_0x5f0540(0x103)][_0x5f0540(0x10d)] = !![]),
+    document[_0x5f0540(0x111)](_0x5f0540(0x115))[_0x5f0540(0x133)](
+      (_0x520561) => {
+        const _0x511e86 = _0x5f0540;
+        _0x520561[_0x511e86(0x109)][_0x511e86(0x121)] = _0x511e86(0x154);
+      }
+    ));
+  if (!_0x3de528[_0x5f0540(0x110)]) {
+    window[_0x5f0540(0x131)][_0x5f0540(0x14c)](
+      'SSO\x20Token\x20is\x20required'
+    ),
+      (_0x4a22b8[_0x5f0540(0x109)][_0x5f0540(0x121)] = _0x5f0540(0x154));
+    return;
+  }
+  try {
+    const _0x4c0dfe = new AbortController(),
+      _0x5e0646 = setTimeout(() => _0x4c0dfe['abort'](), 0x7530),
+      _0x7ef5a8 = await fetch(_0x5d7e74, {
+        method: _0x5f0540(0x11c),
+        headers: { 'Content-Type': _0x5f0540(0x126) },
+        body: JSON[_0x5f0540(0x156)](_0x3de528),
+        signal: _0x4c0dfe[_0x5f0540(0x106)],
+      });
+    clearTimeout(_0x5e0646);
+    const _0x64ad7 = _0x7ef5a8[_0x5f0540(0x104)][_0x5f0540(0x123)](
+      _0x5f0540(0x125)
+    );
+    if (!_0x64ad7 || !_0x64ad7[_0x5f0540(0x138)](_0x5f0540(0x126)))
+      throw new Error(_0x5f0540(0x153));
+    const _0x572a77 = await _0x7ef5a8[_0x5f0540(0x147)]();
+    console['log'](_0x5f0540(0x136) + new Date()['toISOString']()),
+      console[_0x5f0540(0x155)](_0x5f0540(0x11a) + _0x7ef5a8['status']),
+      console[_0x5f0540(0x155)](
+        '[CLIENT]\x20Response\x20size:\x20~' +
+          JSON[_0x5f0540(0x156)](_0x572a77)[_0x5f0540(0x141)] / 0x400 +
+          '\x20KB'
+      );
+    if (!_0x7ef5a8['ok'])
+      throw new Error(
+        _0x572a77[_0x5f0540(0x108)] ||
+          _0x572a77[_0x5f0540(0x118)] ||
+          _0x5f0540(0x124) + _0x7ef5a8[_0x5f0540(0x127)]
+      );
+    if (_0x572a77['error'])
+      window[_0x5f0540(0x131)][_0x5f0540(0x14c)](_0x572a77[_0x5f0540(0x108)]);
+    else
+      _0x572a77['status'] === _0x5f0540(0x108) ?
+        window[_0x5f0540(0x131)][_0x5f0540(0x14c)](
+          _0x572a77['message'] || _0x5f0540(0x135)
+        )
+      : ((window[_0x5f0540(0x103)][_0x5f0540(0x100)] = _0x572a77),
+        window[_0x5f0540(0x131)][_0x5f0540(0x102)](_0x572a77));
+  } catch (_0x563951) {
+    _0x563951[_0x5f0540(0x11f)] === 'AbortError' ?
+      window[_0x5f0540(0x131)]['displayError'](_0x5f0540(0x13e))
+    : (window['uiAPI'][_0x5f0540(0x14c)](
+        _0x5f0540(0x124) + (_0x563951[_0x5f0540(0x118)] || _0x5f0540(0x148))
+      ),
+      console['error'](_0x5f0540(0x140), _0x563951));
+  } finally {
+    _0x4a22b8[_0x5f0540(0x109)][_0x5f0540(0x121)] = 'none';
+  }
+}
+function formatDuration(_0x58e5da) {
+  const _0x547bb4 = a0_0x3e5ceb;
+  if (!_0x58e5da || isNaN(_0x58e5da)) return _0x58e5da;
+  const _0x2efa31 = parseFloat(_0x58e5da),
+    _0x5a8e77 = Math[_0x547bb4(0x11d)](_0x2efa31 / 0x15180),
+    _0x31860f = Math[_0x547bb4(0x11d)]((_0x2efa31 % 0x15180) / 0xe10),
+    _0x40f2d9 = Math[_0x547bb4(0x11d)]((_0x2efa31 % 0xe10) / 0x3c),
+    _0x364d47 = Math[_0x547bb4(0x11d)](_0x2efa31 % 0x3c);
+  return (
+    _0x5a8e77 +
+    _0x547bb4(0x107) +
+    _0x31860f +
+    _0x547bb4(0x137) +
+    _0x40f2d9 +
+    _0x547bb4(0x10b) +
+    _0x364d47 +
+    '\x20Seconds'
+  );
+}
+function formatEpochTime(_0x2ad881, _0x376584) {
+  const _0x564df4 = a0_0x3e5ceb;
+  if (!_0x2ad881) return _0x2ad881;
+  const _0x2eb117 = parseInt(_0x2ad881);
+  if (isNaN(_0x2eb117)) return _0x2ad881;
+  const _0x403f03 =
+    _0x2eb117['toString']()['length'] <= 0xa ? _0x2eb117 * 0x3e8 : _0x2eb117;
+  let _0x56476d = 0x0;
+  if (_0x376584 !== _0x564df4(0x128)) {
+    const _0x34e2a2 = _0x376584['match'](/GMT([+-])(\d+)(?::(\d+))?/);
+    if (_0x34e2a2) {
+      const _0x27099d = _0x34e2a2[0x1] === '+' ? 0x1 : -0x1,
+        _0x16eec3 = parseInt(_0x34e2a2[0x2]),
+        _0x3bff6c = _0x34e2a2[0x3] ? parseInt(_0x34e2a2[0x3]) : 0x0;
+      _0x56476d = _0x27099d * (_0x16eec3 * 0x3c + _0x3bff6c) * 0x3c * 0x3e8;
+    }
+  }
+  const _0x56f46a = new Date(_0x403f03 + _0x56476d);
+  return _0x56f46a[_0x564df4(0x14d)]()[_0x564df4(0x12e)](
+    _0x564df4(0x132),
+    _0x376584
+  );
+}
+function a0_0x1776(_0x51f135, _0x4e9ae9) {
+  const _0x424e92 = a0_0x424e();
+  return (
+    (a0_0x1776 = function (_0x177606, _0x203857) {
+      _0x177606 = _0x177606 - 0x100;
+      let _0x3fd5d4 = _0x424e92[_0x177606];
+      return _0x3fd5d4;
+    }),
+    a0_0x1776(_0x51f135, _0x4e9ae9)
+  );
+}
+function processTimestamps(
+  _0x13d227,
+  _0x1f087c,
+  _0x345fbc = [
+    a0_0x3e5ceb(0x122),
+    a0_0x3e5ceb(0x13b),
+    a0_0x3e5ceb(0x10c),
+    'utcEndSeconds',
+    'timestamp',
+    'startTime',
+    a0_0x3e5ceb(0x12f),
+  ],
+  _0x3c9b02 = [
+    a0_0x3e5ceb(0x143),
+    'timePlayedTotal',
+    a0_0x3e5ceb(0x130),
+    'avgLifeTime',
+    'duration',
+    a0_0x3e5ceb(0x120),
+  ]
+) {
+  const _0x4c2013 = a0_0x3e5ceb;
+  if (!_0x13d227 || typeof _0x13d227 !== _0x4c2013(0x101)) return _0x13d227;
+  if (Array[_0x4c2013(0x10e)](_0x13d227))
+    return _0x13d227[_0x4c2013(0x11b)]((_0x2088ab) =>
+      processTimestamps(_0x2088ab, _0x1f087c, _0x345fbc, _0x3c9b02)
+    );
+  const _0x134b47 = {};
+  for (const [_0x1a08ca, _0x13b85e] of Object[_0x4c2013(0x14a)](_0x13d227)) {
+    if (
+      _0x345fbc[_0x4c2013(0x138)](_0x1a08ca) &&
+      typeof _0x13b85e === _0x4c2013(0x12d)
+    )
+      _0x134b47[_0x1a08ca] = formatEpochTime(_0x13b85e, _0x1f087c);
+    else {
+      if (
+        _0x3c9b02[_0x4c2013(0x138)](_0x1a08ca) &&
+        typeof _0x13b85e === 'number' &&
+        document[_0x4c2013(0x13a)](_0x4c2013(0x144))[_0x4c2013(0x14e)]
+      )
+        _0x134b47[_0x1a08ca] = formatDuration(_0x13b85e);
+      else
+        typeof _0x13b85e === _0x4c2013(0x101) && _0x13b85e !== null ?
+          (_0x134b47[_0x1a08ca] = processTimestamps(
+            _0x13b85e,
+            _0x1f087c,
+            _0x345fbc,
+            _0x3c9b02
+          ))
+        : (_0x134b47[_0x1a08ca] = _0x13b85e);
+    }
+  }
+  return _0x134b47;
+}
diff --git a/src/js/backend.js b/src/js/backend.js
index fc66c6a..cef4b08 100644
--- a/src/js/backend.js
+++ b/src/js/backend.js
@@ -107,11 +107,11 @@ async function fetchData(endpoint, requestData) {
   }
 
   // Validate request data
-  if (!requestData.ssoToken) {
-    window.uiAPI.displayError('SSO Token is required');
-    loadingElement.style.display = 'none';
-    return;
-  }
+  // if (!requestData.ssoToken) {
+  //   window.uiAPI.displayError('SSO Token is required');
+  //   loadingElement.style.display = 'none';
+  //   return;
+  // }
 
   try {
     // Set up the request with a timeout
diff --git a/src/js/frontend.js b/src/js/frontend.js
index fe32726..bc905be 100644
--- a/src/js/frontend.js
+++ b/src/js/frontend.js
@@ -67,8 +67,93 @@ document.addEventListener('DOMContentLoaded', function () {
   setupTimeOptions();
   addSyncListeners();
   initializeSessionTracking();
+  checkDemoMode();
+  // Call initially and then set up a refresh interval
+  updateRequestLimitInfo();
 });
 
+setInterval(updateRequestLimitInfo, 60000); // Update every minute
+
+// Function to check if we're in demo mode
+function checkDemoMode() {
+  console.log('Checking for demo mode...');
+  fetch('/health')
+    .then((response) => {
+      if (!response.ok) {
+        throw new Error(`HTTP error ${response.status}`);
+      }
+      return response.json();
+    })
+    .then((data) => {
+      console.log('Demo mode status:', data.demoMode);
+      if (data.demoMode) {
+        enableDemoMode();
+      }
+    })
+    .catch((error) => {
+      console.error('Error checking demo mode:', error);
+    });
+}
+
+// Function to enable demo mode UI
+function enableDemoMode() {
+  console.log('Enabling demo mode UI...');
+
+  // Get the SSO token input field
+  const ssoTokenInput = document.getElementById('ssoToken');
+  if (ssoTokenInput) {
+    // Disable the input
+    ssoTokenInput.disabled = true;
+    // Add a placeholder indicating demo mode
+    ssoTokenInput.placeholder = 'Demo Mode - No SSO Token Required';
+    // Gray it out with a CSS class
+    ssoTokenInput.classList.add('demo-mode');
+
+    // Add a demo mode indicator above the form
+    const formContainer = document.querySelector('.form-container');
+    if (formContainer) {
+      // Check if notice already exists to avoid duplicates
+      if (!document.querySelector('.demo-notice')) {
+        const demoNotice = document.createElement('div');
+        demoNotice.className = 'demo-notice';
+        demoNotice.innerHTML =
+          '<strong>🎮 Demo Mode Active</strong> - Using a pre-configured token. No need to enter your own SSO token.';
+        formContainer.insertBefore(demoNotice, formContainer.firstChild);
+      }
+    }
+  }
+
+  // Ensure this is applied to window.appState
+  window.appState = window.appState || {};
+  window.appState.demoMode = true;
+}
+
+function updateRequestLimitInfo() {
+  fetch('/health')
+    .then((response) => response.json())
+    .then((data) => {
+      const limitInfoElement = document.querySelector('.request-limit-info');
+      if (data.requestsRemaining !== null) {
+        const message =
+          data.requestsRemaining > 0 ?
+            `Demo mode requests remaining: <strong>${data.requestsRemaining}</strong> (Resets hourly)`
+          : `Request limit reached. Please try again later or use your own SSO token.`;
+
+        if (limitInfoElement) {
+          limitInfoElement.innerHTML = `<small>${message}</small>`;
+        } else {
+          const formContainer = document.querySelector('.form-container');
+          if (formContainer) {
+            const limitInfo = document.createElement('div');
+            limitInfo.className = 'request-limit-info';
+            limitInfo.innerHTML = `<small>${message}</small>`;
+            formContainer.appendChild(limitInfo);
+          }
+        }
+      }
+    });
+}
+
 // Tab switching logic
 function initTabSwitching() {
   document.querySelectorAll('.tab').forEach((tab) => {
diff --git a/src/js/index.d.ts b/src/js/index.d.ts
index 72b99a0..e0a2035 100644
--- a/src/js/index.d.ts
+++ b/src/js/index.d.ts
@@ -68,6 +68,11 @@ declare class MW {
   matchInfo: (matchId: string, platform: platforms) => Promise<unknown>;
   seasonloot: (gamertag: string, platform: platforms) => Promise<unknown>;
   mapList: (platform: platforms) => Promise<unknown>;
+  communityMapDataForMapMode: (
+    mapName: string,
+    gameMode: string,
+    platform: platforms
+  ) => Promise<unknown>;
 }
 declare class MW2 {
   fullData: (unoId: string) => Promise<unknown>;
diff --git a/src/js/index.js b/src/js/index.js
index 96edf38..2e94853 100644
--- a/src/js/index.js
+++ b/src/js/index.js
@@ -55,6 +55,7 @@ let basePostHeaders = {
   'user-agent': userAgent,
 };
 let baseUrl = 'https://profile.callofduty.com';
+let custombaseUrl = 'https://www.callofduty.com';
 let apiPath = '/api/papi-client';
 let baseTelescopeUrl = 'https://telescope.callofduty.com';
 let apiTelescopePath = '/api/ts-api';
@@ -166,6 +167,32 @@ const sendRequest = (url) =>
       throw exception;
     }
   });
+const sendRequestCustom = (url) =>
+  tslib_1.__awaiter(void 0, void 0, void 0, function* () {
+    try {
+      if (!loggedIn) throw new Error('Not Logged In.');
+      let requestUrl = `${custombaseUrl}${apiPath}${url}`;
+      if (debugMode) console.log(`[DEBUG]`, `Request Uri: ${requestUrl}`);
+      if (debugMode) console.time('Round Trip');
+      const { body, statusCode } = yield (0, undici_1.request)(requestUrl, {
+        headers: baseHeaders,
+      });
+      if (debugMode) console.timeEnd('Round Trip');
+      if (statusCode >= 500)
+        throw new Error(
+          `Received status code: '${statusCode}'. Route may be down or not exist.`
+        );
+      let response = yield body.json();
+      if (debugMode)
+        console.log(
+          `[DEBUG]`,
+          `Body Size: ${JSON.stringify(response).length} bytes.`
+        );
+      return response;
+    } catch (exception) {
+      throw exception;
+    }
+  });
 const sendPostRequest = (url, data) =>
   tslib_1.__awaiter(void 0, void 0, void 0, function* () {
     try {
@@ -314,6 +341,8 @@ class Endpoints {
       `/codfriends/v1/${action}/${this.platform}/${this.lookupType}/${this.gamertag}`;
     this.search = () =>
       `/crm/cod/v2/platform/${this.platform}/username/${this.gamertag}/search`;
+    this.communityMapDataForMapMode = (mapName, gameMode) =>
+      `/ce/v1/title/${this.game}/platform/${this.platform}/gameType/${this.mode}/map/${mapName}/mode/${gameMode}/communityMapData`;
     this.game = game;
     this.gamertag = gamertag;
     this.platform = platform;
@@ -607,6 +636,26 @@ class MW {
         return yield sendRequest(endpoint.mapList());
       });
     };
+    this.communityMapDataForMapMode = (mapName, gameMode, platform) => {
+      var gamertag, platform, lookupType;
+      return tslib_1.__awaiter(this, void 0, void 0, function* () {
+        ({
+          gamertag,
+          _platform: platform,
+          lookupType,
+        } = mapGamertagToPlatform('', platform));
+        const endpoint = new Endpoints(
+          games.ModernWarfare,
+          gamertag,
+          platform,
+          modes.Multiplayer,
+          lookupType
+        );
+        return yield sendRequestCustom(
+          endpoint.communityMapDataForMapMode(mapName, gameMode)
+        );
+      });
+    };
   }
 }
 class MW2 {
diff --git a/src/js/index.ts b/src/js/index.ts
index d369c40..cbc41ed 100644
--- a/src/js/index.ts
+++ b/src/js/index.ts
@@ -46,6 +46,7 @@ let basePostHeaders: CustomHeaders = {
 };
 
 let baseUrl: string = 'https://profile.callofduty.com';
+let custombaseUrl = 'https://www.callofduty.com';
 let apiPath: string = '/api/papi-client';
 let baseTelescopeUrl: string = 'https://telescope.callofduty.com';
 let apiTelescopePath: string = '/api/ts-api';
@@ -183,6 +184,39 @@ const sendRequest = async (url: string) => {
   }
 };
 
+const sendRequestCustom = async (url: string) => {
+  try {
+    if (!loggedIn) throw new Error('Not Logged In.');
+    let requestUrl = `${custombaseUrl}${apiPath}${url}`;
+
+    if (debugMode) console.log(`[DEBUG]`, `Request Uri: ${requestUrl}`);
+    if (debugMode) console.time('Round Trip');
+
+    const { body, statusCode } = await request(requestUrl, {
+      headers: baseHeaders,
+    });
+
+    if (debugMode) console.timeEnd('Round Trip');
+
+    if (statusCode >= 500)
+      throw new Error(
+        `Received status code: '${statusCode}'. Route may be down or not exist.`
+      );
+
+    let response = await body.json();
+
+    if (debugMode)
+      console.log(
+        `[DEBUG]`,
+        `Body Size: ${JSON.stringify(response).length} bytes.`
+      );
+
+    return response;
+  } catch (exception: unknown) {
+    throw exception;
+  }
+};
+
 const sendPostRequest = async (url: string, data: string) => {
   try {
     if (!loggedIn) throw new Error('Not Logged In.');
@@ -367,6 +401,8 @@ class Endpoints {
     `/codfriends/v1/${action}/${this.platform}/${this.lookupType}/${this.gamertag}`;
   search = () =>
     `/crm/cod/v2/platform/${this.platform}/username/${this.gamertag}/search`;
+  communityMapDataForMapMode = (mapName: string, gameMode: string) =>
+    `/ce/v1/title/${this.game}/platform/${this.platform}/gameType/${this.mode}/map/${mapName}/mode/${gameMode}/communityMapData`;
 }
 
 class TelescopeEndpoints {
@@ -646,6 +682,28 @@ class MW {
   };
 }
 
+communityMapDataForMapMode = async (
+  mapName: string,
+  gameMode: string,
+  platform: platforms
+) => {
+  var {
+    gamertag,
+    _platform: platform,
+    lookupType,
+  } = mapGamertagToPlatform('', platform);
+  const endpoint = new Endpoints(
+    games.ModernWarfare,
+    gamertag,
+    platform,
+    modes.Multiplayer,
+    lookupType
+  );
+  return await sendRequestCustom(
+    endpoint.communityMapDataForMapMode(mapName, gameMode)
+  );
+};
+
 class MW2 {
   fullData = async (unoId: string) => {
     var { gamertag } = mapGamertagToPlatform(unoId, platforms.Uno, true);
diff --git a/src/js/test.js b/src/js/test.js
new file mode 100644
index 0000000..08c9606
--- /dev/null
+++ b/src/js/test.js
@@ -0,0 +1,41 @@
+import { login, ModernWarfare, platforms } from './index.js';
+import fs from 'fs';
+
+// Login using the SSO token
+const ssoToken =
+  'MTk1NjgyNzA6MTc0NDQ4OTcxNDE4MDpiOGE2MDEwMzY1ZWQ5OTM0NGM3ZjA0MWQxMTFjMTExNA';
+
+(async () => {
+  try {
+    await login(ssoToken); // Ensure login is successful before making requests
+
+    // // Fetch player data
+    // let gamertag = 'Ahrimdon';
+    // let platform = platforms.XBOX; // Use predefined constants for platforms
+
+    // let data = await ModernWarfare.fullData(gamertag, platform);
+    // console.log('Fetched data:', data);
+
+    // // Convert data to a JSON string with formatting
+    // const jsonData = JSON.stringify(data, null, 2);
+
+    // // Write the JSON data to a file
+    // fs.writeFileSync('playerData.json', jsonData);
+
+    // Fetch community map data for a specific map and mode
+    const mapData = await ModernWarfare.communityMapDataForMapMode(
+      'mp_m_king',
+      'gun',
+      platforms.XBOX
+    );
+    console.log('Map data:', mapData);
+
+    // Save to file if needed
+    fs.writeFileSync('mapData.json', JSON.stringify(mapData, null, 2));
+    console.log('Map data has been saved to mapData.json');
+
+    // console.log('Player data has been saved to playerData.json');
+  } catch (error) {
+    console.error('Error fetching data:', error);
+  }
+})();
diff --git a/token.txt b/token.txt
new file mode 100644
index 0000000..bd816fe
--- /dev/null
+++ b/token.txt
@@ -0,0 +1 @@
+<YOUR_SSO_TOKEN_HERE>
\ No newline at end of file