From f03f2f5df4927626e8ef2e9126b5756f7f37f781 Mon Sep 17 00:00:00 2001
From: David Bauer <david.bauer@open-xchange.com>
Date: Thu, 23 Mar 2023 01:03:35 +0100
Subject: [PATCH] Refactoring: Separated fastify plugins and routes

- Unit and Integration tests now use "inject" instead of starting a server
  and using supertest.
- Add Coverage Reports and "test:dev" mode. This enables a mocha --watch
  like workflow using nodemon, as --watch does not work with esm modules.
- Reorganized and renamed files and functions
---
 .dockerignore                          |   6 +-
 .env.defaults                          |   2 +
 .env.test                              |   4 +
 .gitignore                             |   1 -
 .gitlab-ci.yml                         |   6 +
 .ignore                                |   1 -
 integration/caching_test.js            |  13 +-
 integration/config_test.js             |  13 +-
 integration/update-version_test.js     |  23 +-
 okteto-entrypoint.sh                   |   1 -
 src/swagger.yaml => openapi.yaml       |   0
 package.json                           |  46 +-
 performance-tests/all-files.js         |  86 ---
 spec/app_root_test.js                  |  26 +-
 spec/dependencies_test.js              |  17 +-
 spec/file-depencies_test.js            |  31 +-
 spec/file_caching_test.js              | 155 ++---
 spec/headers_test.js                   |  25 +-
 spec/meta_test.js                      |  31 +-
 spec/pwa_test.js                       |  69 +--
 spec/redirect_test.js                  |  18 +-
 spec/redis_test.js                     |  11 +-
 spec/salt_test.js                      |   9 +-
 spec/server_test.js                    |  42 +-
 spec/util.js                           |  34 +-
 spec/version_mismatches_test.js        |  25 +-
 src/cache.js                           |  63 +-
 src/configMap.js                       |  24 -
 src/config_map.js                      |  18 +
 src/create-app.js                      |  87 ---
 src/files.js                           |  27 +-
 src/handlers/version.js                |   9 -
 src/index.js                           |  81 ++-
 src/lightship.js                       |  25 +
 src/manifests.js                       |   2 +-
 src/metrics.js                         |  14 -
 src/plugins/cors.js                    |  11 +
 src/plugins/formbody.js                |   5 +
 src/plugins/helmet.js                  |  22 +
 src/plugins/logging.js                 |  24 +
 src/plugins/metadata.js                |   7 -
 src/plugins/metrics.js                 |  37 ++
 src/plugins/sensible.js                |   5 +
 src/plugins/swagger.js                 |  38 ++
 src/plugins/url-data.js                |   5 +
 src/redis.js                           |  61 +-
 src/routes/autohooks.js                |  28 +
 src/{plugins => routes}/manifests.js   |   4 +-
 src/{meta.js => routes/metadata.js}    |  15 +-
 src/{plugins => routes}/redirects.js   |   8 +-
 src/{plugins => routes}/serve-files.js |   5 +-
 src/{plugins => routes}/webmanifest.js |  95 +--
 src/util.js                            |  17 -
 src/validator.js                       |  14 -
 src/version.js                         |  66 +-
 yarn.lock                              | 811 +++++++++----------------
 56 files changed, 1050 insertions(+), 1273 deletions(-)
 create mode 100644 .env.test
 delete mode 100644 .ignore
 delete mode 100644 okteto-entrypoint.sh
 rename src/swagger.yaml => openapi.yaml (100%)
 delete mode 100644 performance-tests/all-files.js
 delete mode 100644 src/configMap.js
 create mode 100644 src/config_map.js
 delete mode 100644 src/create-app.js
 delete mode 100644 src/handlers/version.js
 create mode 100644 src/lightship.js
 delete mode 100644 src/metrics.js
 create mode 100644 src/plugins/cors.js
 create mode 100644 src/plugins/formbody.js
 create mode 100644 src/plugins/helmet.js
 create mode 100644 src/plugins/logging.js
 delete mode 100644 src/plugins/metadata.js
 create mode 100644 src/plugins/metrics.js
 create mode 100644 src/plugins/sensible.js
 create mode 100644 src/plugins/swagger.js
 create mode 100644 src/plugins/url-data.js
 create mode 100644 src/routes/autohooks.js
 rename src/{plugins => routes}/manifests.js (71%)
 rename src/{meta.js => routes/metadata.js} (56%)
 rename src/{plugins => routes}/redirects.js (50%)
 rename src/{plugins => routes}/serve-files.js (79%)
 rename src/{plugins => routes}/webmanifest.js (83%)
 delete mode 100644 src/validator.js

diff --git a/.dockerignore b/.dockerignore
index 8d91874..9b89962 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,11 +1,10 @@
 .editorconfig
 .env
-.eslintrc
+.eslint*
 .gitignore
 .gitlab-ci.yml
 .gitlab
 .husky
-.mocharc.cjs
 .stignore
 CHANGELOG.md
 docker-compose.yml
@@ -16,3 +15,6 @@ okteto*
 output
 README.md
 spec
+docs
+integration
+jsconfig.json
diff --git a/.env.defaults b/.env.defaults
index a26510f..cce505c 100644
--- a/.env.defaults
+++ b/.env.defaults
@@ -11,3 +11,5 @@ COMPRESS_FILE_TYPES=application/javascript application/json application/x-javasc
 REDIS_PORT=6379
 REDIS_DB=0
 REDIS_PREFIX=ui-middleware
+REDIS_HOST=localhost
+ORIGINS=*
diff --git a/.env.test b/.env.test
new file mode 100644
index 0000000..b7bcb49
--- /dev/null
+++ b/.env.test
@@ -0,0 +1,4 @@
+multi='spec=- xunit=./output/junit.xml'
+LOG_LEVEL=fatal
+MOCHA_FILE=./output/junit.xml
+NODE_NO_WARNINGS=1
diff --git a/.gitignore b/.gitignore
index 2ab0284..5bc5ccd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -73,7 +73,6 @@ web_modules/
 
 # dotenv environment variables file
 .env
-.env.test
 
 # parcel-bundler cache (https://parceljs.org/)
 .cache
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index af7db51..d12631a 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -3,6 +3,12 @@ include:
     file: nodejs.yml
     ref: 3.4.0
 
+unit tests:
+  extends: .unit tests
+  timeout: 10 minutes
+  tags:
+    - build-hetzner
+
 integration tests:
   extends: .unit tests
   services:
diff --git a/.ignore b/.ignore
deleted file mode 100644
index dfa5597..0000000
--- a/.ignore
+++ /dev/null
@@ -1 +0,0 @@
-.gitlab/autodeploy/*
\ No newline at end of file
diff --git a/integration/caching_test.js b/integration/caching_test.js
index 232f3f9..3c82d5d 100644
--- a/integration/caching_test.js
+++ b/integration/caching_test.js
@@ -1,6 +1,5 @@
-import request from 'supertest'
 import { expect } from 'chai'
-import { brotliParser, generateSimpleViteManifest, mockApp, mockConfig, mockFetch } from '../spec/util.js'
+import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch } from '../spec/util.js'
 import * as td from 'testdouble'
 import { getRedisKey } from '../src/util.js'
 import zlib from 'node:zlib'
@@ -19,7 +18,7 @@ describe('File caching service', function () {
       }
     })
     await import('../src/redis.js').then(({ client }) => client.flushdb())
-    app = await mockApp()
+    app = await injectApp()
   })
 
   afterEach(async function () {
@@ -27,7 +26,7 @@ describe('File caching service', function () {
   })
 
   it('caches manifest data', async function () {
-    const response = await request(app.server).get('/manifests').parse(brotliParser)
+    const response = await app.inject({ url: '/manifests' })
     expect(response.statusCode).to.equal(200)
     const version = response.headers.version
 
@@ -38,7 +37,7 @@ describe('File caching service', function () {
   })
 
   it('caches html files', async function () {
-    const response = await request(app.server).get('/index.html')
+    const response = await app.inject({ url: '/index.html' })
     expect(response.statusCode).to.equal(200)
     const version = response.headers.version
 
@@ -55,14 +54,14 @@ describe('File caching service', function () {
     await client.set(getRedisKey({ version, name: '/demo.js:meta' }), '{"headers":{"content-type":"application/javascript","dependencies":false}}')
     await client.set(getRedisKey({ version, name: '/demo.js:body' }), 'console.log("Demo")')
 
-    const response = await request(app.server).get('/demo.js').set('version', version)
+    const response = await app.inject({ url: '/demo.js', headers: { version } })
     expect(response.statusCode).to.equal(200)
 
     // just for testing purposes, delete the keys from redis to make sure, it is served from local cache
     await client.del(getRedisKey({ version, name: '/demo.js:meta' }))
     await client.del(getRedisKey({ version, name: '/demo.js:body' }))
 
-    const response2 = await request(app.server).get('/demo.js').set('version', version)
+    const response2 = await app.inject({ url: '/demo.js', headers: { version } })
     expect(response2.statusCode).to.equal(200)
   })
 })
diff --git a/integration/config_test.js b/integration/config_test.js
index eb62a09..750b631 100644
--- a/integration/config_test.js
+++ b/integration/config_test.js
@@ -1,6 +1,5 @@
-import request from 'supertest'
 import { expect } from 'chai'
-import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch, wait } from '../spec/util.js'
+import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, wait } from '../spec/util.js'
 import * as td from 'testdouble'
 import { getRedisKey } from '../src/util.js'
 
@@ -25,7 +24,7 @@ describe('Configuration', function () {
       }
     })
     await import('../src/redis.js').then(({ client }) => client.flushdb())
-    app = await mockApp()
+    app = await injectApp()
   })
 
   afterEach(async function () {
@@ -39,15 +38,15 @@ describe('Configuration', function () {
   })
 
   it('updates the configuration when updated on a different node', async function () {
-    const response = await request(app.server).get('/meta')
-    expect(response.body).to.have.length(2)
+    const response = await app.inject({ url: '/meta' })
+    expect(response.json()).to.have.length(2)
 
     config.baseUrls = []
     const { pubClient } = await import('../src/redis.js')
     pubClient.publish(getRedisKey({ name: 'updateVersionInfo' }), JSON.stringify({ version: '1234' }))
     await wait(200)
 
-    const response2 = await request(app.server).get('/meta')
-    expect(response2.body).to.have.length(1)
+    const response2 = await app.inject({ url: '/meta' })
+    expect(response2.json()).to.have.length(1)
   })
 })
diff --git a/integration/update-version_test.js b/integration/update-version_test.js
index e1b9d20..bb92cc8 100644
--- a/integration/update-version_test.js
+++ b/integration/update-version_test.js
@@ -1,6 +1,5 @@
-import request from 'supertest'
 import { expect } from 'chai'
-import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch, wait } from '../spec/util.js'
+import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, wait } from '../spec/util.js'
 import * as td from 'testdouble'
 import { getRedisKey } from '../src/util.js'
 
@@ -25,7 +24,7 @@ describe('Updates the version', function () {
       }
     })
     await import('../src/redis.js').then(({ client }) => client.flushdb())
-    app = await mockApp()
+    app = await injectApp()
   })
 
   afterEach(async function () {
@@ -39,7 +38,7 @@ describe('Updates the version', function () {
   })
 
   it('with manually triggered job', async function () {
-    const responseBeforeUpdate = await request(app.server).get('/index.html')
+    const responseBeforeUpdate = await app.inject({ url: '/index.html' })
     expect(responseBeforeUpdate.statusCode).to.equal(200)
     expect(responseBeforeUpdate.headers.version).to.equal('85101541')
 
@@ -49,13 +48,13 @@ describe('Updates the version', function () {
     // update is executed with the second iteration
     expect(await getQueue('update-version').add({}).then(job => job.finished())).to.equal('85102502')
 
-    const responseAfterUpdate = await request(app.server).get('/index.html')
+    const responseAfterUpdate = await app.inject({ url: '/index.html' })
     expect(responseAfterUpdate.statusCode).to.equal(200)
     expect(responseAfterUpdate.headers.version).to.equal('85102502')
   })
 
   it('with automatically triggered job', async function () {
-    const responseBeforeUpdate = await request(app.server).get('/index.html')
+    const responseBeforeUpdate = await app.inject({ url: '/index.html' })
     expect(responseBeforeUpdate.statusCode).to.equal(200)
     expect(responseBeforeUpdate.headers.version).to.equal('85101541')
 
@@ -77,13 +76,13 @@ describe('Updates the version', function () {
       })
     })
 
-    const responseAfterUpdate = await request(app.server).get('/index.html')
+    const responseAfterUpdate = await app.inject({ url: '/index.html' })
     expect(responseAfterUpdate.statusCode).to.equal(200)
     expect(responseAfterUpdate.headers.version).to.equal('85102502')
   })
 
   it('receives version update via redis event', async function () {
-    const responseBeforeUpdate = await request(app.server).get('/index.html')
+    const responseBeforeUpdate = await app.inject({ url: '/index.html' })
     expect(responseBeforeUpdate.statusCode).to.equal(200)
     expect(responseBeforeUpdate.headers.version).to.equal('85101541')
 
@@ -92,7 +91,7 @@ describe('Updates the version', function () {
     pubClient.publish(getRedisKey({ name: 'updateVersionInfo' }), JSON.stringify({ version: '1234' }))
     await wait(10)
 
-    const responseAfterUpdate = await request(app.server).get('/index.html')
+    const responseAfterUpdate = await app.inject({ url: '/index.html' })
     expect(responseAfterUpdate.statusCode).to.equal(200)
     expect(responseAfterUpdate.headers.version).to.equal('1234')
   })
@@ -120,13 +119,13 @@ describe('Updates the version', function () {
       await client.flushdb()
       // preconfigure redis
       await client.set(getRedisKey({ name: 'versionInfo' }), JSON.stringify({ version: '12345' }))
-      app = await mockApp()
+      app = await injectApp()
     })
 
     it('uses version from redis if present', async function () {
-      app = await mockApp()
+      app = await injectApp()
 
-      const response = await request(app.server).get('/index.html')
+      const response = await app.inject({ url: '/index.html' })
       expect(response.statusCode).to.equal(200)
       expect(response.headers.version).to.equal('12345')
     })
diff --git a/okteto-entrypoint.sh b/okteto-entrypoint.sh
deleted file mode 100644
index 3acef19..0000000
--- a/okteto-entrypoint.sh
+++ /dev/null
@@ -1 +0,0 @@
-yarn && yarn dev
diff --git a/src/swagger.yaml b/openapi.yaml
similarity index 100%
rename from src/swagger.yaml
rename to openapi.yaml
diff --git a/package.json b/package.json
index 1de35de..ea8bd1d 100644
--- a/package.json
+++ b/package.json
@@ -5,43 +5,47 @@
   "type": "module",
   "main": "src/index.js",
   "scripts": {
+    "pino-pretty": "npx --yes pino-pretty -t 'SYS:mm/dd HH:MM:ss.l' -x fatal:0,error:3,warn:4,info:6,debug:7,trace:8 -X fatal:red,error:red,warn:yellow,info:green,debug:blue,trace:gray",
     "lint": "eslint . --cache --fix",
     "start": "node src/index.js",
-    "dev": "npx --yes nodemon index.js | npx --yes pino-pretty -t 'SYS:mm/dd HH:MM:ss.l' -x fatal:0,error:3,warn:4,info:6,debug:7,trace:8 -X fatal:red,error:red,warn:yellow,info:green,debug:blue,trace:gray",
+    "dev": "npx --yes nodemon index.js | yarn pino-pretty",
     "prepare": "husky install",
-    "test": "NODE_NO_WARNINGS=1 LOG_LEVEL=fatal mocha --loader=testdouble --config spec/.mocharc.cjs --exit",
-    "integration": "LOG_LEVEL=error mocha --loader=testdouble --config integration/.mocharc.cjs --exit",
+    "test": "npx --yes env-cmd -f .env.test npx --yes c8 mocha --spec=spec/*_test.js",
+    "test:dev": "npx --yes env-cmd -f .env.test npx --yes nodemon -x npx --yes c8 mocha -R progress --spec=spec/*_test.js",
+    "test:debug": "npx --yes env-cmd -f .env.test --no-override mocha ",
+    "integration": "npx --yes env-cmd -f .env.test mocha --config integration/.mocharc.cjs -R spec --spec=integration/*_test.js",
     "release": "yarn --no-progress -s create @open-xchange/release"
   },
   "author": "Open-Xchange",
   "license": "CC-BY-NC-SA-2.5",
   "dependencies": {
+    "@fastify/autoload": "^5.7.1",
+    "@fastify/cors": "^8.2.1",
     "@fastify/formbody": "^7.4.0",
     "@fastify/helmet": "^10.1.0",
+    "@fastify/sensible": "^5.2.0",
     "@fastify/swagger": "^8.3.1",
+    "@fastify/swagger-ui": "^1.5.0",
     "@fastify/url-data": "^5.3.1",
-    "@open-xchange/logging": "^0.1.6",
     "bull": "^4.10.4",
     "dotenv-defaults": "^5.0.2",
     "fastify": "^4.15.0",
     "fastify-metrics": "^10.2.0",
     "fastify-plugin": "^4.5.0",
-    "http-errors": "^2.0.0",
     "ioredis": "^5.3.1",
     "js-yaml": "^4.0.0",
     "lightship": "^7.1.1"
   },
   "devDependencies": {
     "@open-xchange/lint": "^0.0.2",
-    "autocannon": "^7.10.0",
+    "@types/ioredis-mock": "^8.2.1",
     "chai": "^4.3.7",
     "ioredis-mock": "^8.2.3",
     "mocha": "^10.2.0",
-    "nodemon": "^2.0.22",
+    "mocha-junit-reporter": "^2.2.0",
+    "mocha-multi": "^1.1.7",
     "sinon": "^15.0.3",
-    "superagent": "^8.0.9",
-    "supertest": "^6.3.3",
-    "testdouble": "^3.17.2"
+    "testdouble": "^3.17.1"
   },
   "lint-staged": {
     "*.js": "eslint --cache --fix"
@@ -53,5 +57,27 @@
   },
   "engines": {
     "node": ">=16"
+  },
+  "c8": {
+    "all": true,
+    "exclude": [
+      "src/plugins/**/*",
+      "spec/**/*",
+      "integration/**/*",
+      "node_modules/**/*",
+      "src/index.js",
+      "src/lightship.js"
+    ],
+    "reporter": [
+      "cobertura",
+      "text"
+    ],
+    "report-dir": "output/coverage"
+  },
+  "mocha": {
+    "reporter": "mocha-multi",
+    "loader": "testdouble",
+    "exit": true,
+    "recursive": true
   }
 }
diff --git a/performance-tests/all-files.js b/performance-tests/all-files.js
deleted file mode 100644
index 0547a34..0000000
--- a/performance-tests/all-files.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import fs from 'node:fs/promises'
-import { Worker } from 'node:worker_threads'
-
-import { config } from 'dotenv-defaults'
-import autocannon from 'autocannon'
-import { wait } from '../spec/util'
-
-config()
-
-if (!process.env.FRONTEND_PATH) throw new Error('You need to set "process.env.FRONTEND_PATH"')
-const frontendPath = process.env.FRONTEND_PATH
-
-// 1. check if a UI is present (env variable and http check)
-const result = await fetch(new URL('manifest.json', frontendPath))
-if (!result.ok) throw new Error(`Cannot find UI at "${frontendPath}"`)
-
-const configPath = './config/config.yaml'
-const oldConfigPath = './config/config-old.yaml'
-
-if (!process.env.UI_MIDDLEWARE_PATH) {
-// 2. create configuration file, store old config
-  try {
-    await fs.stat(configPath)
-    await fs.copyFile(configPath, oldConfigPath)
-  } catch (e) {}
-
-  fs.writeFile(configPath, `
-baseUrls:
-  - ${frontendPath}
-`)
-
-  // 3. start the ui-middleware
-  const worker = new Worker('./src/index.js', { env: { LOG_LEVEL: 'warn' } })
-  worker.on('error', err => console.error(err))
-  process.env.UI_MIDDLEWARE_PATH = `http://localhost:${process.env.PORT}/`
-}
-const uiMWPath = process.env.UI_MIDDLEWARE_PATH
-
-// 4. collect manifests from the ui-container (or already do that in 1.)
-const manifests = await result.json()
-
-// 5.1 setup autocannon with cold cache
-console.log('Setup finished, start autocannon...')
-await wait(50)
-const coldCacheResult = await autocannon({
-  url: uiMWPath,
-  connections: 1,
-  duration: 60,
-  requests: Object.values(manifests).map(({ file }) => ({
-    path: new URL(file, uiMWPath).href
-  })),
-  workers: 5
-})
-
-// 6.1 handle result
-await wait(50)
-console.log('Autocannon results with cold cache:')
-console.log(autocannon.printResult(coldCacheResult))
-
-// 5.2 setup autocannon options with all files
-console.log('Setup finished, start autocannon with warm cache...')
-await wait(50)
-const warmCacheResult = await autocannon({
-  url: uiMWPath,
-  connections: 1,
-  duration: 60,
-  requests: Object.values(manifests).map(({ file }) => ({
-    path: new URL(file, uiMWPath).href
-  })),
-  workers: 5
-})
-
-// 6.2 handle result
-await wait(50)
-console.log('Autocannon results with warm cache:')
-console.log(autocannon.printResult(warmCacheResult))
-
-// 7. restore old config
-try {
-  await fs.stat(oldConfigPath)
-  await fs.copyFile(oldConfigPath, configPath)
-  await fs.rm(oldConfigPath)
-} catch (e) {}
-
-// force exit, because the server is still running
-process.exit(0)
diff --git a/spec/app_root_test.js b/spec/app_root_test.js
index 029da1e..67151e4 100644
--- a/spec/app_root_test.js
+++ b/spec/app_root_test.js
@@ -1,5 +1,4 @@
-import request from 'supertest'
-import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch, mockRedis } from './util.js'
+import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, mockRedis } from './util.js'
 import { expect } from 'chai'
 import * as td from 'testdouble'
 import RedisMock from 'ioredis-mock'
@@ -41,7 +40,7 @@ describe('With different app root', function () {
       }
     })
     process.env.APP_ROOT = '/appsuite/'
-    app = await mockApp()
+    app = await injectApp()
   })
 
   afterEach(async function () {
@@ -51,37 +50,38 @@ describe('With different app root', function () {
   })
 
   it('serves files defined in manifest.json file', async function () {
-    const response = await request(app.server).get('/appsuite/example.js')
+    const response = await app.inject({ url: '/appsuite/example.js' })
     expect(response.statusCode).to.equal(200)
     expect(response.headers['content-type']).to.equal('application/javascript')
-    expect(response.text).to.equal('this is example')
-    const response2 = await request(app.server).get('/appsuite/test.txt')
+    expect(response.body).to.equal('this is example')
+
+    const response2 = await app.inject({ url: '/appsuite/test.txt' })
     expect(response2.statusCode).to.equal(200)
     expect(response2.headers['content-type']).to.equal('text/plain')
-    expect(response2.text).to.equal('this is test')
+    expect(response2.body).to.equal('this is test')
   })
 
   it('serves / as index.html', async function () {
-    const response = await request(app.server).get('/appsuite/')
+    const response = await app.inject({ url: '/appsuite/' })
     expect(response.statusCode).to.equal(200)
     expect(response.headers['content-type']).to.equal('text/html')
-    expect(response.text).to.equal('<html><head></head><body>it\'s me</body></html>')
+    expect(response.body).to.equal('<html><head></head><body>it\'s me</body></html>')
   })
 
   it('serves approot without slash as index.html', async function () {
-    const response = await request(app.server).get('/appsuite')
+    const response = await app.inject({ url: '/appsuite' })
     expect(response.statusCode).to.equal(302)
     expect(response.headers.location).to.equal('/appsuite/')
   })
 
   it('directly fetches files not referenced in manifest.json files from the upstream servers', async function () {
-    const response = await request(app.server).get('/appsuite/favicon.ico')
+    const response = await app.inject({ url: '/appsuite/favicon.ico' })
     expect(response.statusCode).to.equal(200)
-    expect(response.text).to.equal('not really a favicon, though')
+    expect(response.body).to.equal('not really a favicon, though')
   })
 
   it('returns 404 if a file misses the app-root', async function () {
-    const response = await request(app.server).get('/example.js')
+    const response = await app.inject({ url: '/example.js' })
     expect(response.statusCode).to.equal(404)
   })
 })
diff --git a/spec/dependencies_test.js b/spec/dependencies_test.js
index f7b109a..3c43441 100644
--- a/spec/dependencies_test.js
+++ b/spec/dependencies_test.js
@@ -18,14 +18,8 @@ describe('Vite manifest parsing', function () {
           '_preload-helper-a7bbbf37.js'
         ],
         meta: {
-          gettext: {
-            dictionary: true
-          },
-          manifests: [
-            {
-              namespace: 'i18n'
-            }
-          ]
+          gettext: { dictionary: true },
+          manifests: [{ namespace: 'i18n' }]
         }
       },
       'io.ox/guidedtours/intro.js': {
@@ -53,9 +47,7 @@ describe('Vite manifest parsing', function () {
         ],
         meta: {
           manifests: [
-            {
-              namespace: 'settings'
-            },
+            { namespace: 'settings' },
             {
               namespace: 'io.ox/core/main',
               title: 'Guided tours',
@@ -65,8 +57,7 @@ describe('Vite manifest parsing', function () {
               settings: false,
               index: 100,
               package: 'open-xchange-guidedtours'
-            }
-          ]
+            }]
         }
       },
       'io.ox/guidedtours/multifactor.js': {
diff --git a/spec/file-depencies_test.js b/spec/file-depencies_test.js
index db2ab6e..31143a1 100644
--- a/spec/file-depencies_test.js
+++ b/spec/file-depencies_test.js
@@ -1,5 +1,4 @@
-import request from 'supertest'
-import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch, mockRedis } from './util.js'
+import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, mockRedis } from './util.js'
 import { expect } from 'chai'
 import * as td from 'testdouble'
 import RedisMock from 'ioredis-mock'
@@ -29,7 +28,7 @@ describe('JS files with dependencies contain events', function () {
         '/main.css': () => new Response('.foo { color: #000; }', { headers: { 'content-type': 'text/css' } })
       }
     })
-    app = await mockApp()
+    app = await injectApp()
   })
 
   afterEach(async function () {
@@ -39,8 +38,8 @@ describe('JS files with dependencies contain events', function () {
   })
 
   it('javascript files from different versions have correct dependencies', async function () {
-    const r1 = await request(app.server).get('/index.html.js')
-    expect(r1.headers.dependencies).to.equal('main.css')
+    const r1 = await app.inject({ url: '/index.html.js' })
+    expect(r1.headers.dependencies[0]).to.equal('main.css')
 
     mockFetchConfig['http://ui-server']['/manifest.json'] = generateSimpleViteManifest({
       'example.js': {},
@@ -59,16 +58,22 @@ describe('JS files with dependencies contain events', function () {
       await updateVersionProcessor()
     })
 
-    const r2 = await request(app.server).get('/index.html.js')
-    expect(r2.headers.dependencies).to.equal('other.css')
+    const r2 = await app.inject({ url: '/index.html.js' })
+    expect(r2.headers.dependencies[0]).to.equal('other.css')
 
-    const r3 = await request(app.server).get('/index.html.js').set('version', r1.headers.version)
-    expect(r3.headers.dependencies).to.equal('main.css')
+    const r3 = await app.inject({
+      url: '/index.html.js',
+      headers: { version: r1.headers.version }
+    })
+    expect(r3.headers.dependencies[0]).to.equal('main.css')
 
-    const r4 = await request(app.server).get('/index.html.js').set('version', r2.headers.version)
-    expect(r4.headers.dependencies).to.equal('other.css')
+    const r4 = await app.inject({
+      url: '/index.html.js',
+      headers: { version: r2.headers.version }
+    })
+    expect(r4.headers.dependencies[0]).to.equal('other.css')
 
-    const r5 = await request(app.server).get('/index.html.js')
-    expect(r5.headers.dependencies).to.equal('other.css')
+    const r5 = await app.inject({ url: '/index.html.js' })
+    expect(r5.headers.dependencies[0]).to.equal('other.css')
   })
 })
diff --git a/spec/file_caching_test.js b/spec/file_caching_test.js
index 1cf64bd..0523bcc 100644
--- a/spec/file_caching_test.js
+++ b/spec/file_caching_test.js
@@ -1,5 +1,4 @@
-import request from 'supertest'
-import { brotliParser, generateSimpleViteManifest, mockApp, mockConfig, mockFetch, mockRedis, wait } from './util.js'
+import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, mockRedis, wait } from './util.js'
 import fs from 'fs'
 import { expect } from 'chai'
 import * as td from 'testdouble'
@@ -56,7 +55,7 @@ describe('File caching service', function () {
         }
       }
     })
-    app = await mockApp()
+    app = await injectApp()
   })
 
   afterEach(async function () {
@@ -65,47 +64,47 @@ describe('File caching service', function () {
   })
 
   it('serves files defined in manifest.json file', async function () {
-    const response = await request(app.server).get('/example.js')
+    const response = await app.inject({ url: '/example.js' })
     expect(response.statusCode).to.equal(200)
     expect(response.headers['content-type']).to.equal('application/javascript')
-    expect(response.text).to.equal('this is example')
+    expect(response.body).to.equal('this is example')
     // expect(response.headers['content-security-policy']).to.contain('sha256-NzZhMTE2Njc2YTgyNTZmZTdlZGVjZDU3YTNmYzRjNmM1OWZkMTI2NjRkYzZmMWM3YTkwMGU3ZTdhNDlhZmVlMwo=')
-    const response2 = await request(app.server).get('/test.txt')
+    const response2 = await app.inject({ url: '/test.txt' })
     expect(response2.statusCode).to.equal(200)
     expect(response2.headers['content-type']).to.equal('text/plain')
-    expect(response2.text).to.equal('this is test')
+    expect(response2.body).to.equal('this is test')
   })
 
   it('serves css files', async function () {
-    const response = await request(app.server).get('/main.css')
+    const response = await app.inject({ url: '/main.css' })
     expect(response.statusCode).to.equal(200)
     expect(response.headers['content-type']).to.equal('text/css')
     // expect(response.headers['content-security-policy']).to.contain('sha256-YjRiYWRlYTVhYmM5ZTZkNjE2ZGM4YjcwZWRlNzUxMmU0YjgxY2UxMWExOTI2ZjM1NzM1M2Y2MWJjNmUwMmZjMwo=')
   })
 
   it('serves / as index.html', async function () {
-    const response = await request(app.server).get('/')
+    const response = await app.inject({ url: '/' })
     expect(response.statusCode).to.equal(200)
     expect(response.headers['content-type']).to.equal('text/html')
-    expect(response.text).to.equal('<html><head></head><body>it\'s me</body></html>')
+    expect(response.body).to.equal('<html><head></head><body>it\'s me</body></html>')
   })
 
   it('directly fetches files not referenced in manifest.json files from the upstream servers', async function () {
-    const response = await request(app.server).get('/favicon.ico')
+    const response = await app.inject({ url: '/favicon.ico' })
     expect(response.statusCode).to.equal(200)
-    expect(response.text).to.equal('not really a favicon, though')
+    expect(response.body).to.equal('not really a favicon, though')
   })
 
   it('returns 404 if file can not be resolved', async function () {
-    const response = await request(app.server).get('/unknown-file.txt')
+    const response = await app.inject({ url: '/unknown-file.txt' })
     expect(response.statusCode).to.equal(404)
   })
 
   it('serves binary files', async function () {
-    const response = await request(app.server).get('/image.png')
+    const response = await app.inject({ url: '/image.png' })
     expect(response.statusCode).to.equal(200)
     expect(response.body.length === imageStat.size)
-    expect(response.body).to.deep.equal(image)
+    expect(response.rawPayload).to.deep.equal(image)
   })
 
   it('only fetches files once', async function () {
@@ -118,13 +117,13 @@ describe('File caching service', function () {
         })
       }
     })
-    app = await mockApp()
+    app = await injectApp()
 
     expect(spy.callCount).to.equal(0)
-    let response = await request(app.server).get('/example.js')
+    let response = await app.inject({ url: '/example.js' })
     expect(response.statusCode).to.equal(200)
     expect(spy.callCount).to.equal(1)
-    response = await request(app.server).get('/example.js')
+    response = await app.inject({ url: '/example.js' })
     expect(response.statusCode).to.equal(200)
     expect(spy.callCount).to.equal(1)
   })
@@ -139,17 +138,17 @@ describe('File caching service', function () {
         })
       }
     })
-    app = await mockApp()
+    app = await injectApp()
 
     expect(spy.callCount).to.equal(0)
-    let response = await request(app.server).get('/example.js')
+    let response = await app.inject({ url: '/example.js' })
     expect(response.statusCode).to.equal(200)
     expect(spy.callCount).to.equal(1)
     // delete file from redis
     await redis.client.del(`${response.headers.version}:/example.js:body`)
     await redis.client.del(`${response.headers.version}:/example.js:meta`)
     // and fetch once more
-    response = await request(app.server).get('/example.js')
+    response = await app.inject({ url: '/example.js' })
     expect(response.statusCode).to.equal(200)
     expect(spy.callCount).to.equal(1)
   })
@@ -169,22 +168,22 @@ describe('File caching service', function () {
         })
       }
     })
-    app = await mockApp()
+    app = await injectApp()
     expect(spy.callCount).to.equal(0)
-    let response = await request(app.server).get('/image.png')
+    let response = await app.inject({ url: '/image.png' })
     expect(response.statusCode).to.equal(200)
-    expect(response.body).to.deep.equal(image)
+    expect(response.rawPayload).to.deep.equal(image)
     expect(spy.callCount).to.equal(1)
-    response = await request(app.server).get('/image.png')
+    response = await app.inject({ url: '/image.png' })
     expect(response.statusCode).to.equal(200)
-    expect(response.body).to.deep.equal(image)
+    expect(response.rawPayload).to.deep.equal(image)
     expect(spy.callCount).to.equal(1)
   })
 
   it('a file is not cached again, if loaded from cache', async function () {
     const spy = sandbox.spy(redis.client, 'set')
 
-    let response = await request(app.server).get('/example.js')
+    let response = await app.inject({ url: '/example.js' })
     expect(response.statusCode).to.equal(200)
 
     // called 4 times.
@@ -193,7 +192,7 @@ describe('File caching service', function () {
     // two times for for example.js (meta and body)
     expect(spy.callCount).to.equal(4)
 
-    response = await request(app.server).get('/example.js')
+    response = await app.inject({ url: '/example.js' })
     expect(response.statusCode).to.equal(200)
 
     // should still be called 4 times, because everything is in cache
@@ -222,9 +221,9 @@ describe('File caching service', function () {
         })
       }
     })
-    app = await mockApp()
+    app = await injectApp()
 
-    const response = await request(app.server).get('/example.js')
+    const response = await app.inject({ url: '/example.js' })
     expect(response.statusCode).to.equal(200)
     expect(spy1.callCount).to.equal(0)
     expect(spy2.callCount).to.equal(1)
@@ -243,29 +242,38 @@ describe('File caching service', function () {
         )
       }
     })
-    app = await mockApp()
+    app = await injectApp()
 
-    const response1 = await request(app.server).get('/example.js').set('version', '1234')
+    const response1 = await app.inject({
+      url: '/example.js',
+      headers: { version: '1234' }
+    })
     expect(response1.statusCode).to.equal(200)
-    expect(response1.text).to.equal('first')
+    expect(response1.body).to.equal('first')
 
-    const response2 = await request(app.server).get('/example.js')
+    const response2 = await app.inject({ url: '/example.js' })
     expect(response2.statusCode).to.equal(200)
-    expect(response2.text).to.equal('second')
+    expect(response2.body).to.equal('second')
 
     const latestVersion = response2.headers['latest-version']
 
-    const response3 = await request(app.server).get('/example.js').set('version', '1234')
+    const response3 = await app.inject({
+      url: '/example.js',
+      headers: { version: '1234' }
+    })
     expect(response3.statusCode).to.equal(200)
-    expect(response3.text).to.equal('first')
+    expect(response3.body).to.equal('first')
 
-    const response4 = await request(app.server).get('/example.js')
+    const response4 = await app.inject({ url: '/example.js' })
     expect(response4.statusCode).to.equal(200)
-    expect(response4.text).to.equal('second')
+    expect(response4.body).to.equal('second')
 
-    const response5 = await request(app.server).get('/example.js').set('version', latestVersion)
+    const response5 = await app.inject({
+      url: '/example.js',
+      headers: { version: latestVersion }
+    })
     expect(response5.statusCode).to.equal(200)
-    expect(response5.text).to.equal('second')
+    expect(response5.body).to.equal('second')
   })
 
   it('checks again for files after an error occurred', async function () {
@@ -281,14 +289,14 @@ describe('File caching service', function () {
         )
       }
     })
-    app = await mockApp()
+    app = await injectApp()
 
-    const response1 = await request(app.server).get('/example.js')
+    const response1 = await app.inject({ url: '/example.js' })
     expect(response1.statusCode).to.equal(404)
 
-    const response2 = await request(app.server).get('/example.js')
+    const response2 = await app.inject({ url: '/example.js' })
     expect(response2.statusCode).to.equal(200)
-    expect(response2.text).to.equal('Now available')
+    expect(response2.body).to.equal('Now available')
   })
 
   it('does not check again, when a 404 occurred', async function () {
@@ -304,12 +312,12 @@ describe('File caching service', function () {
         )
       }
     })
-    app = await mockApp()
+    app = await injectApp()
 
-    const response1 = await request(app.server).get('/example.js')
+    const response1 = await app.inject({ url: '/example.js' })
     expect(response1.statusCode).to.equal(404)
 
-    const response2 = await request(app.server).get('/example.js')
+    const response2 = await app.inject({ url: '/example.js' })
     expect(response2.statusCode).to.equal(404)
   })
 
@@ -323,19 +331,19 @@ describe('File caching service', function () {
         })
       }
     })
-    app = await mockApp()
+    app = await injectApp()
 
     expect(spy.callCount).to.equal(0)
     const [res1, res2] = await Promise.all([
-      request(app.server).get('/example.js'),
-      request(app.server).get('/example.js')
+      app.inject({ url: '/example.js' }),
+      app.inject({ url: '/example.js' })
     ])
     expect(res1.statusCode).to.equal(200)
     expect(res2.statusCode).to.equal(200)
     expect(spy.callCount).to.equal(1)
   })
 
-  it('only fetches manifests once, even when requested simultanously', async function () {
+  it('only fetches manifests once, even when requested simultaneously', async function () {
     let spy
     mockFetch({
       'http://ui-server': {
@@ -346,12 +354,12 @@ describe('File caching service', function () {
         '/example.js': () => new Response('this is example', { headers: { 'content-type': 'application/javascript' } })
       }
     })
-    app = await mockApp()
+    app = await injectApp()
 
     expect(spy.callCount).to.equal(0)
     const [res1, res2] = await Promise.all([
-      request(app.server).get('/manifests').parse(brotliParser),
-      request(app.server).get('/example.js')
+      app.inject({ url: '/manifests' }),
+      app.inject({ url: '/example.js' })
     ])
     expect(res1.statusCode).to.equal(200)
     expect(res2.statusCode).to.equal(200)
@@ -381,12 +389,12 @@ describe('File caching service', function () {
           })
         }
       })
-      app = await mockApp()
+      app = await injectApp()
 
       expect(spy.callCount).to.equal(0)
       const [res1, res2] = await Promise.all([
-        request(app.server).get('/example.js'),
-        request(app.server).get('/example.js')
+        app.inject({ url: '/example.js' }),
+        app.inject({ url: '/example.js' })
       ])
       expect(res1.statusCode).to.equal(200)
       expect(res2.statusCode).to.equal(200)
@@ -401,14 +409,13 @@ describe('File caching service', function () {
         '/index.html': () => new Response([...new Array(2500)].join(' '), { headers: { 'content-type': 'text/html' } })
       }
     })
-    app = await mockApp()
+    app = await injectApp()
 
-    const response = await request(app.server).get('/index.html')
+    const response = await app.inject({ url: '/index.html' })
     expect(response.statusCode).to.equal(200)
     expect(response.headers['content-encoding']).to.equal('gzip')
-
     // check for files in redis
-    expect(zlib.gunzipSync(await redis.client.getBuffer('ui-middleware:554855300:/index.html:body')).toString()).to.equal(response.text)
+    expect(zlib.gunzipSync(await redis.client.getBuffer('ui-middleware:554855300:/index.html:body')).toString()).to.equal(zlib.gunzipSync(response.rawPayload).toString())
   })
 
   it('serves files as brotli compressed', async function () {
@@ -418,14 +425,14 @@ describe('File caching service', function () {
         '/large.js': () => new Response([...new Array(2500)].join('a'), { headers: { 'content-type': 'application/javascript' } })
       }
     })
-    app = await mockApp()
+    app = await injectApp()
 
-    const response = await request(app.server).get('/large.js')
+    const response = await app.inject({ url: '/large.js' })
     expect(response.statusCode).to.equal(200)
     expect(response.headers['content-encoding']).to.equal('br')
 
     // check for files in redis
-    expect((await redis.client.getBuffer('ui-middleware:554855300:/large.js:body')).toString()).to.equal(response.text)
+    expect((await redis.client.getBuffer('ui-middleware:554855300:/large.js:body')).toString()).to.equal(response.body)
   })
 
   it('does not serve small files with compression', async function () {
@@ -435,14 +442,14 @@ describe('File caching service', function () {
         '/small.js': () => new Response('small', { headers: { 'content-type': 'application/javascript' } })
       }
     })
-    app = await mockApp()
+    app = await injectApp()
 
-    const response = await request(app.server).get('/small.js')
+    const response = await app.inject({ url: '/small.js' })
     expect(response.statusCode).to.equal(200)
     expect(response.headers).to.not.have.property('content-encoding')
 
     // check for files in redis
-    expect((await redis.client.getBuffer('ui-middleware:554855300:/small.js:body')).toString()).to.equal(response.text)
+    expect((await redis.client.getBuffer('ui-middleware:554855300:/small.js:body')).toString()).to.equal(response.body)
   })
 
   it('does not serve other mime types with compression', async function () {
@@ -452,14 +459,14 @@ describe('File caching service', function () {
         '/file.mp3': () => new Response('123', { headers: { 'content-type': 'audio/mpeg' } })
       }
     })
-    app = await mockApp()
+    app = await injectApp()
 
-    const response = await request(app.server).get('/file.mp3')
+    const response = await app.inject({ url: '/file.mp3' })
     expect(response.statusCode).to.equal(200)
     expect(response.headers).to.not.have.property('content-encoding')
 
     // check for files in redis
-    expect(await redis.client.getBuffer('ui-middleware:554855300:/file.mp3:body')).to.deep.equal(response.body)
+    expect(await redis.client.getBuffer('ui-middleware:554855300:/file.mp3:body')).to.deep.equal(response.rawPayload)
   })
 
   it('does serve svg with brotli compression (also escapes chars in regex)', async function () {
@@ -469,13 +476,13 @@ describe('File caching service', function () {
         '/file.svg': () => new Response([...new Array(2500)].join(' '), { headers: { 'content-type': 'image/svg+xml' } })
       }
     })
-    app = await mockApp()
+    app = await injectApp()
 
-    const response = await request(app.server).get('/file.svg')
+    const response = await app.inject({ url: '/file.svg' })
     expect(response.statusCode).to.equal(200)
     expect(response.headers['content-encoding']).to.equal('br')
 
     // check for files in redis
-    expect(await redis.client.getBuffer('ui-middleware:554855300:/file.svg:body')).to.deep.equal(response.body)
+    expect(await redis.client.getBuffer('ui-middleware:554855300:/file.svg:body')).to.deep.equal(response.rawPayload)
   })
 })
diff --git a/spec/headers_test.js b/spec/headers_test.js
index faca45b..abc4c72 100644
--- a/spec/headers_test.js
+++ b/spec/headers_test.js
@@ -1,5 +1,4 @@
-import request from 'supertest'
-import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch, mockRedis } from './util.js'
+import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, mockRedis } from './util.js'
 import { expect } from 'chai'
 import * as td from 'testdouble'
 import RedisMock from 'ioredis-mock'
@@ -28,7 +27,7 @@ describe('Responses contain custom headers', function () {
       }
     })
     await mockRedis().isReady()
-    app = await mockApp()
+    app = await injectApp()
   })
 
   afterEach(async function () {
@@ -41,22 +40,22 @@ describe('Responses contain custom headers', function () {
   })
 
   it('index.html has version', async function () {
-    const response = await request(app.server).get('/index.html')
+    const response = await app.inject({ url: '/index.html' })
     expect(response.statusCode).to.equal(200)
     expect(response.headers.version).to.equal('3215668592')
   })
 
   it('serves requested version', async function () {
-    const response = await request(app.server).get('/index.html.js').set('version', '123456')
+    const response = await app.inject({ url: '/index.html.js', headers: { version: '123456' } })
     expect(response.statusCode).to.equal(200)
     expect(response.headers.version).to.equal('123456')
     expect(response.headers['latest-version']).to.equal('3215668592')
   })
 
   it('javascript file contains dependencies', async function () {
-    const response = await request(app.server).get('/index.html.js')
+    const response = await app.inject({ url: '/index.html.js' })
     expect(response.statusCode).to.equal(200)
-    expect(response.headers.dependencies).to.equal('main.css')
+    expect(response.headers.dependencies[0]).to.equal('main.css')
   })
 
   describe('with different files', function () {
@@ -74,11 +73,11 @@ describe('Responses contain custom headers', function () {
           '/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } })
         }
       })
-      app = await mockApp()
+      app = await injectApp()
     })
 
     it('index.html has version', async function () {
-      const response = await request(app.server).get('/index.html')
+      const response = await app.inject({ url: '/index.html' })
       expect(response.statusCode).to.equal(200)
       // important here is, that it is different than in the test without meta.json
       expect(response.headers.version).to.equal('3961519424')
@@ -101,11 +100,11 @@ describe('Responses contain custom headers', function () {
           '/meta.json': { commitSha: '1234567890' }
         }
       })
-      app = await mockApp()
+      app = await injectApp()
     })
 
     it('index.html has version', async function () {
-      const response = await request(app.server).get('/index.html')
+      const response = await app.inject({ url: '/index.html' })
       expect(response.statusCode).to.equal(200)
       // important here is, that it is different than in the test without meta.json
       expect(response.headers.version).to.equal('1487554813')
@@ -128,11 +127,11 @@ describe('Responses contain custom headers', function () {
           '/meta.json': { commitSha: '0987654321' }
         }
       })
-      app = await mockApp()
+      app = await injectApp()
     })
 
     it('index.html has version', async function () {
-      const response = await request(app.server).get('/index.html')
+      const response = await app.inject({ url: '/index.html' })
       expect(response.statusCode).to.equal(200)
       // important here is, that it is different than in the test without meta.json
       expect(response.headers.version).to.equal('319344871')
diff --git a/spec/meta_test.js b/spec/meta_test.js
index b9ca8a1..e9a2437 100644
--- a/spec/meta_test.js
+++ b/spec/meta_test.js
@@ -1,5 +1,4 @@
-import request from 'supertest'
-import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch, mockRedis } from './util.js'
+import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, mockRedis } from './util.js'
 import { expect } from 'chai'
 import * as td from 'testdouble'
 import RedisMock from 'ioredis-mock'
@@ -21,7 +20,7 @@ describe('Responses contain custom headers', function () {
         '/meta.json': { name: 'sample-service', version: '1.0' }
       }
     })
-    app = await mockApp()
+    app = await injectApp()
   })
 
   afterEach(async function () {
@@ -37,9 +36,9 @@ describe('Responses contain custom headers', function () {
     process.env.BUILD_TIMESTAMP = '0123456789'
     process.env.CI_COMMIT_SHA = '0123456789abcdef'
 
-    const response = await request(app.server).get('/meta')
+    const response = await app.inject({ url: '/meta' })
     expect(response.statusCode).to.equal(200)
-    expect(response.body).to.deep.contain({
+    expect(response.json()).to.deep.contain({
       id: 'ui-middleware',
       name: 'UI Middleware',
       buildDate: '0123456789',
@@ -49,17 +48,17 @@ describe('Responses contain custom headers', function () {
   })
 
   it('has metadata from another ui service if available', async function () {
-    const response = await request(app.server).get('/meta')
+    const response = await app.inject({ url: '/meta' })
     expect(response.statusCode).to.equal(200)
-    expect(response.body).to.deep.contain({
+    expect(response.json()).to.deep.contain({
       name: 'sample-service',
       version: '1.0'
     })
   })
 
   it('has updated metadata if config is updated', async function () {
-    const response = await request(app.server).get('/meta')
-    expect(response.body).to.have.length(2)
+    const response = await app.inject({ url: '/meta' })
+    expect(response.json()).to.have.length(2)
 
     config.baseUrls = []
     await import('../src/version.js').then(async ({ updateVersionProcessor }) => {
@@ -68,8 +67,8 @@ describe('Responses contain custom headers', function () {
       await updateVersionProcessor()
     })
 
-    const response2 = await request(app.server).get('/meta')
-    expect(response2.body).to.have.length(1)
+    const response2 = await app.inject({ url: '/meta' })
+    expect(response2.json()).to.have.length(1)
   })
 
   describe('without service avaible', function () {
@@ -79,9 +78,9 @@ describe('Responses contain custom headers', function () {
 
     it('does not have metadata from ui service when unavailable', async function () {
       await import('../src/cache.js').then(({ clear }) => clear())
-      const response = await request(app.server).get('/meta')
+      const response = await app.inject({ url: '/meta' })
       expect(response.statusCode).to.equal(200)
-      expect(response.body).to.not.deep.contain({
+      expect(response.json()).to.not.deep.contain({
         name: 'sample-service',
         version: '1.0'
       })
@@ -102,13 +101,13 @@ describe('Responses contain custom headers', function () {
           '/meta.json': { name: 'sample-service', version: '1.0' }
         }
       })
-      app = await mockApp()
+      app = await injectApp()
     })
 
     it('has metadata', async function () {
-      const response = await request(app.server).get('/meta')
+      const response = await app.inject({ url: '/meta' })
       expect(response.statusCode).to.equal(200)
-      expect(response.body).to.deep.contain({
+      expect(response.json()).to.deep.contain({
         name: 'sample-service',
         version: '1.0'
       })
diff --git a/spec/pwa_test.js b/spec/pwa_test.js
index e27d9c5..1fedfbe 100644
--- a/spec/pwa_test.js
+++ b/spec/pwa_test.js
@@ -1,12 +1,13 @@
-import request from 'supertest'
 import { expect } from 'chai'
 import * as td from 'testdouble'
-import { mockApp, mockConfig, mockFetch, mockRedis } from './util.js'
+import { injectApp, mockConfig, mockFetch, mockRedis } from './util.js'
 
 describe('Service delivers a generated web-manifest', function () {
+  let app
   before(async function () {
     mockConfig({ urls: ['http://ui-server/'] })
     mockRedis()
+    app = await injectApp()
   })
 
   after(async function () {
@@ -19,7 +20,6 @@ describe('Service delivers a generated web-manifest', function () {
   })
 
   it('delivers valid webmanifest with short syntax', async function () {
-    const app = await mockApp()
     mockFetch({
       'https://ui-server': {
         '/api/apps/manifests': {
@@ -39,25 +39,23 @@ describe('Service delivers a generated web-manifest', function () {
         }
       }
     })
-    const response = await request(app.server).get('/pwa.json').set('host', 'ui-server')
+    const response = await app.inject({ url: '/pwa.json', headers: { host: 'ui-server' } })
     expect(response.statusCode).to.equal(200)
-    expect(response.body).to.deep.include({
+    expect(response.headers['content-type']).to.equal('application/manifest+json; charset=utf-8')
+    expect(response.json()).to.deep.include({
       name: 'Valid App Suite',
       short_name: 'Valid App Suite',
-      icons: [
-        {
-          src: '/mycustomlogo.png',
-          type: 'image/png',
-          sizes: '512x512',
-          purpose: 'any'
-        }
-      ],
+      icons: [{
+        src: '/mycustomlogo.png',
+        type: 'image/png',
+        sizes: '512x512',
+        purpose: 'any'
+      }],
       background_color: 'white'
     })
   })
 
   it('delivers no manifest with pwa.enabled=false', async function () {
-    const app = await mockApp()
     mockFetch({
       'https://ui-server': {
         '/api/apps/manifests': {
@@ -69,13 +67,12 @@ describe('Service delivers a generated web-manifest', function () {
         }
       }
     })
-    const response = await request(app.server).get('/pwa.json').set('host', 'ui-server')
+    const response = await app.inject({ url: '/pwa.json', headers: { host: 'ui-server' } })
     expect(response.statusCode).to.equal(200)
-    expect(response.body).to.deep.equal({})
+    expect(response.json()).to.deep.equal({})
   })
 
   it('delivers valid webmanifest with minimal properties', async function () {
-    const app = await mockApp()
     mockFetch({
       'https://ui-server': {
         '/api/apps/manifests': {
@@ -93,9 +90,9 @@ describe('Service delivers a generated web-manifest', function () {
         }
       }
     })
-    const response = await request(app.server).get('/pwa.json').set('host', 'ui-server')
+    const response = await app.inject({ url: '/pwa.json', headers: { host: 'ui-server' } })
     expect(response.statusCode).to.equal(200)
-    expect(response.body).to.deep.include({
+    expect(response.json()).to.deep.include({
       name: 'Short Name',
       short_name: 'Short Name',
       icons: [
@@ -111,7 +108,6 @@ describe('Service delivers a generated web-manifest', function () {
   })
 
   it('must not deliver an invalid manifest when using the short syntax', async function () {
-    const app = await mockApp()
     mockFetch({
       'https://ui-server': {
         '/api/apps/manifests': {
@@ -131,20 +127,18 @@ describe('Service delivers a generated web-manifest', function () {
         }
       }
     })
-    const response = await request(app.server).get('/pwa.json').set('host', 'ui-server')
+    const response = await app.inject({ url: '/pwa.json', headers: { host: 'ui-server' } })
     expect(response.statusCode).to.equal(500)
-    expect(response.text).to.have.string('Failed to load config for url https://ui-server/pwa.json: Error:')
+    expect(response.body).to.have.string('Failed to load config for url https://ui-server/pwa.json: Error:')
   })
 
   it('must not deliver a manifest with invalid host', async function () {
-    const app = await mockApp()
-    const response = await request(app.server).get('/pwa.json').set('host', 'ui-server-not')
+    const response = await app.inject({ url: '/pwa.json', headers: { host: 'ui-server-not' } })
     expect(response.statusCode).to.equal(500)
-    expect(response.text).to.equal('Failed to load config for url https://ui-server-not/pwa.json: Error: Failed to fetch https://ui-server-not/api/apps/manifests?action=config')
+    expect(response.body).to.equal('Failed to load config for url https://ui-server-not/pwa.json: Error: Failed to fetch https://ui-server-not/api/apps/manifests?action=config')
   })
 
   it('delivers valid webmanifest with raw_manifest', async function () {
-    const app = await mockApp()
     mockFetch({
       'https://ui-server': {
         '/api/apps/manifests': {
@@ -172,9 +166,9 @@ describe('Service delivers a generated web-manifest', function () {
         }
       }
     })
-    const response = await request(app.server).get('/pwa.json').set('host', 'ui-server')
+    const response = await app.inject({ url: '/pwa.json', headers: { host: 'ui-server' } })
     expect(response.statusCode).to.equal(200)
-    expect(response.body).to.deep.include({
+    expect(response.json()).to.deep.include({
       name: 'Valid App Suite',
       short_name: 'Valid App Suite',
       icons: [
@@ -190,7 +184,6 @@ describe('Service delivers a generated web-manifest', function () {
   })
 
   it('must not deliver an invalid manifest with raw_manifest', async function () {
-    const app = await mockApp()
     mockFetch({
       'https://ui-server': {
         '/api/apps/manifests': {
@@ -217,13 +210,12 @@ describe('Service delivers a generated web-manifest', function () {
         }
       }
     })
-    const response = await request(app.server).get('/pwa.json').set('host', 'ui-server')
+    const response = await app.inject({ url: '/pwa.json', headers: { host: 'ui-server' } })
     expect(response.statusCode).to.equal(500)
-    expect(response.text).to.have.string('Failed to load config for url https://ui-server/pwa.json: Error:')
+    expect(response.body).to.have.string('Failed to load config for url https://ui-server/pwa.json: Error:')
   })
 
   it('must choose raw_manifest over short syntax', async function () {
-    const app = await mockApp()
     mockFetch({
       'https://ui-server': {
         '/api/apps/manifests': {
@@ -256,9 +248,9 @@ describe('Service delivers a generated web-manifest', function () {
         }
       }
     })
-    const response = await request(app.server).get('/pwa.json').set('host', 'ui-server')
+    const response = await app.inject({ url: '/pwa.json', headers: { host: 'ui-server' } })
     expect(response.statusCode).to.equal(200)
-    expect(response.body).to.deep.equal({
+    expect(response.json()).to.deep.equal({
       name: 'Raw Manifest',
       short_name: 'raw_manifest',
       icons: [
@@ -274,7 +266,6 @@ describe('Service delivers a generated web-manifest', function () {
   })
 
   it('differ between two hosts', async function () {
-    const app = await mockApp()
     mockFetch({
       'https://ui-server': {
         '/api/apps/manifests': {
@@ -302,10 +293,10 @@ describe('Service delivers a generated web-manifest', function () {
         }
       }
     })
-    const response = await request(app.server).get('/pwa.json').set('host', 'ui-server')
-    const responseOther = await request(app.server).get('/pwa.json').set('host', 'ui-server-other')
+    const response = await app.inject({ url: '/pwa.json', headers: { host: 'ui-server' } })
+    const responseOther = await app.inject({ url: '/pwa.json', headers: { host: 'ui-server-other' } })
     expect(response.statusCode).to.equal(200)
-    expect(response.body).to.deep.equal({
+    expect(response.json()).to.deep.equal({
       name: 'OX App Suite',
       short_name: 'OX App Suite',
       icons: [
@@ -330,7 +321,7 @@ describe('Service delivers a generated web-manifest', function () {
       ]
     })
     expect(responseOther.statusCode).to.equal(200)
-    expect(responseOther.body).to.deep.equal({
+    expect(responseOther.json()).to.deep.equal({
       name: 'Other Suite',
       short_name: 'Other Suite',
       icons: [
diff --git a/spec/redirect_test.js b/spec/redirect_test.js
index 0baf750..0e20fbc 100644
--- a/spec/redirect_test.js
+++ b/spec/redirect_test.js
@@ -1,5 +1,4 @@
-import request from 'supertest'
-import { generateSimpleViteManifest, mockConfig, mockFetch, mockApp, mockRedis } from './util.js'
+import { generateSimpleViteManifest, mockConfig, mockFetch, injectApp, mockRedis } from './util.js'
 import { expect } from 'chai'
 import * as td from 'testdouble'
 import RedisMock from 'ioredis-mock'
@@ -16,7 +15,7 @@ describe('Redirects', function () {
         '/example.js': ''
       }
     })
-    app = await mockApp()
+    app = await injectApp()
   })
 
   afterEach(async function () {
@@ -28,19 +27,26 @@ describe('Redirects', function () {
   })
 
   it('without requested location', async function () {
-    const response = await request(app.server).post('/redirect')
+    const response = await app.inject({ method: 'POST', url: '/redirect' })
     expect(response.statusCode).to.equal(302)
     expect(response.headers.location).to.equal('../busy.html')
   })
 
   it('with requested location', async function () {
-    const response = await request(app.server).post('/redirect').send('location=/appsuite/whatever/path')
+    const response = await app.inject({
+      method: 'POST',
+      url: '/redirect',
+      payload: 'location=/appsuite/whatever/path',
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
+      }
+    })
     expect(response.statusCode).to.equal(302)
     expect(response.headers.location).to.equal('/appsuite/whatever/path')
   })
 
   it('redirects /ui to /', async function () {
-    const response = await request(app.server).get('/ui')
+    const response = await app.inject({ url: '/ui' })
     expect(response.statusCode).to.equal(302)
     expect(response.headers.location).to.equal('/')
   })
diff --git a/spec/redis_test.js b/spec/redis_test.js
index cc51b84..7af4f67 100644
--- a/spec/redis_test.js
+++ b/spec/redis_test.js
@@ -1,8 +1,8 @@
-import request from 'supertest'
-import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch, mockRedis } from './util.js'
+import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, mockRedis } from './util.js'
 import { expect } from 'chai'
 import * as td from 'testdouble'
 import sinon from 'sinon'
+import RedisMock from 'ioredis-mock'
 
 const sandbox = sinon.createSandbox()
 
@@ -21,19 +21,20 @@ describe('Redis', function () {
         })
       }
     })
-    app = await mockApp()
+    app = await injectApp()
   })
 
   afterEach(async function () {
     td.reset()
+    await new RedisMock().flushdb()
   })
 
   it('use internal cache, when redis is disabled', async function () {
     expect(spy.callCount).to.equal(0)
-    let response = await request(app.server).get('/example.js')
+    let response = await app.inject({ url: '/example.js' })
     expect(response.statusCode).to.equal(200)
     expect(spy.callCount).to.equal(1)
-    response = await request(app.server).get('/example.js')
+    response = await app.inject({ url: '/example.js' })
     expect(response.statusCode).to.equal(200)
     expect(spy.callCount).to.equal(1)
   })
diff --git a/spec/salt_test.js b/spec/salt_test.js
index a3b3632..88fbe75 100644
--- a/spec/salt_test.js
+++ b/spec/salt_test.js
@@ -1,5 +1,4 @@
-import request from 'supertest'
-import { brotliParser, generateSimpleViteManifest, mockApp, mockConfig, mockFetch, mockRedis } from './util.js'
+import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, mockRedis } from './util.js'
 import { expect } from 'chai'
 import * as td from 'testdouble'
 import RedisMock from 'ioredis-mock'
@@ -17,7 +16,7 @@ describe('Salt', function () {
         '/example.js': ''
       }
     })
-    app = await mockApp()
+    app = await injectApp()
   })
 
   afterEach(async function () {
@@ -26,7 +25,7 @@ describe('Salt', function () {
   })
 
   it('change version when salt changes', async function () {
-    const response = await request(app.server).get('/manifests').parse(brotliParser)
+    const response = await app.inject({ url: '/manifests' })
     expect(response.statusCode).to.equal(200)
     expect(response.headers.version).to.equal('1916675216')
 
@@ -39,7 +38,7 @@ describe('Salt', function () {
       await updateVersionProcessor()
     })
 
-    const responseAfterUpdate = await request(app.server).get('/manifests').parse(brotliParser)
+    const responseAfterUpdate = await app.inject({ url: '/manifests' })
     expect(responseAfterUpdate.statusCode).to.equal(200)
     expect(responseAfterUpdate.headers.version).to.equal('1916675216-1')
   })
diff --git a/spec/server_test.js b/spec/server_test.js
index 0e01528..0983bae 100644
--- a/spec/server_test.js
+++ b/spec/server_test.js
@@ -1,5 +1,4 @@
-import request from 'supertest'
-import { brotliParser, generateSimpleViteManifest, mockApp, mockConfig, mockFetch, mockRedis, wait } from './util.js'
+import { decompressBrotli, generateSimpleViteManifest, injectApp, mockConfig, mockFetch, mockRedis, wait } from './util.js'
 import { expect } from 'chai'
 import * as td from 'testdouble'
 import RedisMock from 'ioredis-mock'
@@ -17,7 +16,7 @@ describe('UI Middleware', function () {
         '/example.js': ''
       }
     })
-    app = await mockApp()
+    app = await injectApp()
   })
 
   afterEach(async function () {
@@ -26,16 +25,19 @@ describe('UI Middleware', function () {
   })
 
   it('fetches manifest data', async function () {
-    const response = await request(app.server).get('/manifests').parse(brotliParser)
+    const response = await app.inject({ url: '/manifests' })
+    const body = await decompressBrotli(response.rawPayload)
     expect(response.statusCode).to.equal(200)
     expect(response.headers['content-encoding']).to.equal('br')
-    expect(response.body).to.deep.equal([{ namespace: 'test', path: 'example' }])
+    expect(body).to.deep.equal([{ namespace: 'test', path: 'example' }])
   })
 
   it('caches manifest data when configuration changes', async function () {
-    const response = await request(app.server).get('/manifests').parse(brotliParser)
+    const response = await app.inject({ url: '/manifests' })
+    const body = await decompressBrotli(response.rawPayload)
     expect(response.statusCode).to.equal(200)
-    expect(response.body).to.deep.equal([{ namespace: 'test', path: 'example' }])
+    expect(response.headers['content-encoding']).to.equal('br')
+    expect(body).to.deep.equal([{ namespace: 'test', path: 'example' }])
 
     fetchConfig['http://ui-server'] = {
       '/manifest.json': generateSimpleViteManifest({ 'example.js': 'other' }),
@@ -44,9 +46,11 @@ describe('UI Middleware', function () {
 
     await wait(150)
 
-    const response2 = await request(app.server).get('/manifests').parse(brotliParser)
+    const response2 = await app.inject({ url: '/manifests' })
+    const body2 = await decompressBrotli(response.rawPayload)
     expect(response2.statusCode).to.equal(200)
-    expect(response2.body).to.deep.equal([{ namespace: 'test', path: 'example' }])
+    expect(response2.headers['content-encoding']).to.equal('br')
+    expect(body2).to.deep.equal([{ namespace: 'test', path: 'example' }])
   })
 
   describe('multiple configurations', function () {
@@ -56,20 +60,18 @@ describe('UI Middleware', function () {
         '/manifest.json': generateSimpleViteManifest({ 'example2.js': 'thing' }),
         '/example2.js': ''
       }
-      app = await mockApp()
+      app = await injectApp()
     })
 
     it('can load multiple configurations', async function () {
-      await request(app.server)
-        .get('/manifests')
-        .parse(brotliParser)
-        .then(response => {
-          expect(response.statusCode).to.equal(200)
-          expect(response.body).to.deep.equal([
-            { namespace: 'test', path: 'example' },
-            { namespace: 'thing', path: 'example2' }
-          ])
-        })
+      const response = await app.inject({ url: '/manifests' })
+      const body = await decompressBrotli(response.rawPayload)
+      expect(response.statusCode).to.equal(200)
+      expect(response.headers['content-encoding']).to.equal('br')
+      expect(body).to.deep.equal([
+        { namespace: 'test', path: 'example' },
+        { namespace: 'thing', path: 'example2' }
+      ])
     })
   })
 })
diff --git a/spec/util.js b/spec/util.js
index c29bab7..04a3a34 100644
--- a/spec/util.js
+++ b/spec/util.js
@@ -3,6 +3,17 @@ import { register } from 'prom-client'
 import RedisMock from 'ioredis-mock'
 import zlib from 'node:zlib'
 import yaml from 'js-yaml'
+import fastify from 'fastify'
+import autoLoad from '@fastify/autoload'
+import sensible from '@fastify/sensible'
+import urlData from '@fastify/url-data'
+import formbody from '@fastify/formbody'
+
+import { fileURLToPath } from 'node:url'
+import { dirname, join } from 'node:path'
+
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = dirname(__filename)
 
 export function generateSimpleViteManifest (mapping) {
   const viteManifest = {}
@@ -54,16 +65,19 @@ export function mockRedis (data = {}, isEnabled = true) {
   return mock
 }
 
-export async function mockApp () {
+export async function injectApp () {
   register.clear()
-  const { createApp } = await import('../src/create-app.js')
-  const { configMap } = await import('../src/configMap.js')
+  const { configMap } = await import('../src/config_map.js')
   const { getLatestVersion } = await import('../src/version.js')
 
   await configMap.load()
   await getLatestVersion()
-  const app = await createApp()
-  await app.listen({ port: 0 })
+  const app = fastify({ disableRequestLogging: true })
+  app.register(sensible)
+  app.register(urlData)
+  app.register(formbody)
+  app.register(autoLoad, { dir: join(__dirname, '../src/routes'), prefix: process.env.APP_ROOT, autoHooks: true })
+
   return app
 }
 
@@ -81,6 +95,16 @@ export async function brotliParser (res, cb) {
   })
 }
 
+export async function decompressBrotli (encodedData) {
+  const compressedData = Buffer.from(encodedData)
+  return new Promise((resolve, reject) => {
+    zlib.brotliDecompress(compressedData, (err, data) => {
+      if (err) reject(err)
+      else resolve(JSON.parse(data.toString('utf8')))
+    })
+  })
+}
+
 export async function wait (timeout) {
   return new Promise(resolve => setTimeout(resolve, timeout))
 }
diff --git a/spec/version_mismatches_test.js b/spec/version_mismatches_test.js
index 236f767..5b4e0a8 100644
--- a/spec/version_mismatches_test.js
+++ b/spec/version_mismatches_test.js
@@ -1,5 +1,4 @@
-import request from 'supertest'
-import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch, mockRedis } from './util.js'
+import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, mockRedis } from './util.js'
 import { expect } from 'chai'
 import * as td from 'testdouble'
 import RedisMock from 'ioredis-mock'
@@ -28,7 +27,7 @@ describe('version mismatches', function () {
         )
       }
     })
-    app = await mockApp()
+    app = await injectApp()
   })
 
   afterEach(async function () {
@@ -38,37 +37,37 @@ describe('version mismatches', function () {
 
   it('detects version mismatches when files are fetched', async function () {
     // get foo.js with initial version
-    let response = await request(app.server).get('/foo.js')
+    let response = await app.inject({ url: '/foo.js' })
     expect(response.statusCode).to.equal(200)
-    expect(response.text).to.equal('foo1')
+    expect(response.body).to.equal('foo1')
     expect(response.headers.version).to.equal('85101541')
 
     // get bar.js. This will cause the server to detect the version mismatch
-    response = await request(app.server).get('/bar.js')
+    response = await app.inject({ url: '/bar.js' })
     expect(response.statusCode).to.equal(404)
 
     // get foo.js again. Since the versions should coincide now, the client should receive the new file
-    response = await request(app.server).get('/foo.js')
+    response = await app.inject({ url: '/foo.js' })
     expect(response.statusCode).to.equal(200)
-    expect(response.text).to.equal('foo2')
+    expect(response.body).to.equal('foo2')
     expect(response.headers.version).to.equal('85102502')
   })
 
   it('detects version mismatches in files not referenced in manifest.json when files are fetched', async function () {
     // get foo.js with initial version
-    let response = await request(app.server).get('/foo.js')
+    let response = await app.inject({ url: '/foo.js' })
     expect(response.statusCode).to.equal(200)
-    expect(response.text).to.equal('foo1')
+    expect(response.body).to.equal('foo1')
     expect(response.headers.version).to.equal('85101541')
 
     // get bar.js. This will cause the server to detect the version mismatch
-    response = await request(app.server).get('/whatever.js')
+    response = await app.inject({ url: '/whatever.js' })
     expect(response.statusCode).to.equal(404)
 
     // get foo.js again. Since the versions should coincide now, the client should receive the new file
-    response = await request(app.server).get('/foo.js')
+    response = await app.inject({ url: '/foo.js' })
     expect(response.statusCode).to.equal(200)
-    expect(response.text).to.equal('foo2')
+    expect(response.body).to.equal('foo2')
     expect(response.headers.version).to.equal('85102502')
   })
 })
diff --git a/src/cache.js b/src/cache.js
index 222529a..7d958d8 100644
--- a/src/cache.js
+++ b/src/cache.js
@@ -1,4 +1,3 @@
-import { createWritable } from './files.js'
 import logger from './logger.js'
 import * as redis from './redis.js'
 import { getRedisKey } from './util.js'
@@ -6,24 +5,22 @@ import { Gauge } from 'prom-client'
 
 const cache = {}
 
+function createWritable (body) {
+  return (typeof body !== 'string' && !(body instanceof Buffer)) ? JSON.stringify(body) : body
+}
+
 export const fileCacheSizeGauge = new Gauge({
   name: 'file_cache_size',
   help: 'Number of entries in file cache'
 })
 
-function logAndIgnoreError (err) {
-  logger.error(err)
-}
-
 export function set (key, value, timeout) {
   logger.debug(`[Cache] Set ${key}`)
   if (cache[key] === value) return
   cache[key] = value
   if (timeout) setTimeout(expire, timeout * 1000, key)
-  if (redis.isEnabled()) {
-    if (timeout) return redis.client.set(key, value, 'EX', timeout).catch(logAndIgnoreError)
-    return redis.client.set(key, value).catch(logAndIgnoreError)
-  }
+  if (timeout) return redis.client.set(key, value, 'EX', timeout).catch(err => logger.error(err))
+  return redis.client.set(key, value).catch(err => logger.error(err))
 }
 
 export async function clear () {
@@ -33,24 +30,22 @@ export async function clear () {
   fileCacheSizeGauge.reset()
 }
 
-export function get (key, fallback) {
+export async function get (key, fallback) {
   if (cache[key]) {
     logger.debug(`[Cache] Resolve from memory: ${key}`)
     return cache[key]
   }
 
   const promise = (async () => {
-    if (redis.isEnabled()) {
-      let result = await redis.client.get(key).catch(err => logger.error(err))
-      if (result) {
-        logger.debug(`[Cache] Resolve from redis: ${key}`)
-        result = JSON.parse(result)
-        cache[key] = result
-        redis.client.ttl(key).then(timeout => {
-          if (timeout > 0) setTimeout(expire, timeout * 1000, key)
-        })
-        return result
-      }
+    let result = await redis.client.get(key).catch(err => logger.error(err))
+    if (result) {
+      logger.debug(`[Cache] Resolve from redis: ${key}`)
+      result = JSON.parse(result)
+      cache[key] = result
+      redis.client.ttl(key).then(timeout => {
+        if (timeout > 0) setTimeout(expire, timeout * 1000, key)
+      })
+      return result
     }
 
     if (!fallback) return
@@ -85,20 +80,18 @@ export function getFile ({ name, version }, fallback) {
     const bodyKey = getRedisKey({ version, name: `${name}:body` })
     const metaKey = getRedisKey({ version, name: `${name}:meta` })
 
-    if (redis.isEnabled()) {
-      const [body, meta = '{}'] = await Promise.all([
-        redis.client.getBuffer(bodyKey),
-        redis.client.get(metaKey)
-      ]).catch((err) => {
-        logger.error(`[Cache] could not access redis: ${err}`)
-        return []
-      })
+    const [body, meta = '{}'] = await Promise.all([
+      redis.client.getBuffer(bodyKey),
+      redis.client.get(metaKey)
+    ]).catch((err) => {
+      logger.error(`[Cache] could not access redis: ${err}`)
+      return []
+    })
 
-      if (body) {
-        logger.debug(`[Cache] Resolve file from redis: ${key}`)
-        fileCacheSizeGauge.inc()
-        return (cache[key] = { body, ...JSON.parse(meta) })
-      }
+    if (body) {
+      logger.debug(`[Cache] Resolve file from redis: ${key}`)
+      fileCacheSizeGauge.inc()
+      return (cache[key] = { body, ...JSON.parse(meta) })
     }
 
     const dataFromServer = await fallback({ version, name }).catch(err => {
@@ -106,7 +99,7 @@ export function getFile ({ name, version }, fallback) {
       throw err
     })
 
-    if (redis.isEnabled()) {
+    {
       logger.debug(`[Cache] Store file in redis: ${key}`)
       const { body, ...rest } = dataFromServer
       redis.client.set(bodyKey, createWritable(body)).catch(err => logger.error(`[Cache] could not store ${bodyKey}: ${err}`))
diff --git a/src/configMap.js b/src/configMap.js
deleted file mode 100644
index 422633a..0000000
--- a/src/configMap.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import fs from 'fs/promises'
-import yaml from 'js-yaml'
-import logger from './logger.js'
-
-class Config {
-  async load () {
-    const doc = yaml.load(await fs.readFile('./config/config.yaml', 'utf8'))
-    // @ts-ignore
-    this._urls = doc.baseUrls
-    // @ts-ignore
-    this._salt = doc.salt
-    logger.debug('[Config] Config has been loaded')
-  }
-
-  get urls () {
-    return this._urls || []
-  }
-
-  get salt () {
-    return this._salt
-  }
-}
-
-export const configMap = new Config()
diff --git a/src/config_map.js b/src/config_map.js
new file mode 100644
index 0000000..1b27e37
--- /dev/null
+++ b/src/config_map.js
@@ -0,0 +1,18 @@
+import fs from 'fs/promises'
+import yaml from 'js-yaml'
+import logger from './logger.js'
+
+export const configMap = {
+  urls: [],
+  salt: null,
+  async load () {
+    try {
+      const doc = yaml.load(await fs.readFile('./config/config.yaml', 'utf8'))
+      this.urls = doc.baseUrls || []
+      this.salt = doc.salt
+      logger.debug('[Config] Config has been loaded')
+    } catch (error) {
+      logger.error(`[Config] Error loading configuration: ${error.message}`)
+    }
+  }
+}
diff --git a/src/create-app.js b/src/create-app.js
deleted file mode 100644
index 1c6a27b..0000000
--- a/src/create-app.js
+++ /dev/null
@@ -1,87 +0,0 @@
-import fastify from 'fastify'
-import formBodyPlugin from '@fastify/formbody'
-import urlDataPlugin from '@fastify/url-data'
-import fastifySwagger from '@fastify/swagger'
-import fastifyMetrics from 'fastify-metrics'
-import helmet from '@fastify/helmet'
-import yaml from 'js-yaml'
-import fs from 'node:fs'
-import { randomUUID } from 'node:crypto'
-
-import versionHandler from './handlers/version.js'
-import manifestsPlugin from './plugins/manifests.js'
-import metadataPlugin from './plugins/metadata.js'
-import redirectsPlugin from './plugins/redirects.js'
-import serveFilePlugin from './plugins/serve-files.js'
-import serveWebmanifest from './plugins/webmanifest.js'
-import logger from './logger.js'
-
-const swaggerDocument = yaml.load(fs.readFileSync('./src/swagger.yaml', 'utf8'))
-
-export async function createApp (basePath) {
-  const app = fastify({
-    logger,
-    connectionTimeout: 30000,
-    disableRequestLogging: true,
-    // genreqId is used to generate a request id if one is not provided by the proxy
-    genReqId: () => randomUUID()
-    // This is the name of the header used to pass a request id from a proxy to the service
-    // requestIdHeader: 'x-request-id',
-  })
-
-  app.addHook('onError', (req, reply, { message, stack, statusCode }, done) => {
-    const error = { statusCode, stack }
-    reply.log.error(error, message || 'request errored')
-    done()
-  })
-
-  // Logs the request with the 'debug' level and also logs headers with the 'trace' level
-  app.addHook('onResponse', (req, reply, done) => {
-    const loggingOptions = { url: req.raw.url, res: reply, responseTime: reply.getResponseTime() }
-    if (process.env.LOG_LEVEL === 'trace') loggingOptions.headers = req.headers
-    reply.log.debug(loggingOptions, 'request completed')
-    done()
-  })
-
-  await app.register(formBodyPlugin)
-  await app.register(urlDataPlugin)
-  await app.register(helmet, {
-    contentSecurityPolicy: false,
-    crossOriginEmbedderPolicy: false,
-    originAgentCluster: false,
-    crossOriginOpenerPolicy: { policy: 'same-origin-allow-popups' }
-  })
-
-  const swaggerConfiguration = {
-    exposeRoute: process.env.EXPOSE_API_DOCS === 'true',
-    routePrefix: '/api-docs',
-    prefix: process.env.APP_ROOT,
-    swagger: swaggerDocument
-  }
-
-  await app.register(fastifySwagger, swaggerConfiguration)
-
-  await app.register(fastifyMetrics, {
-    // do not expose metrics endpoint
-    endpoint: null,
-    routeMetrics: {
-      routeBlacklist: ['/api-docs']
-    }
-  })
-
-  await app.addHook('preHandler', versionHandler)
-
-  await app.register(manifestsPlugin, { prefix: process.env.APP_ROOT })
-  await app.register(metadataPlugin, { prefix: process.env.APP_ROOT })
-  await app.register(redirectsPlugin, { prefix: process.env.APP_ROOT })
-  if (process.env.APP_ROOT.length > 1) {
-    app.get(process.env.APP_ROOT.slice(0, -1), async (req, res) => {
-      res.redirect(process.env.APP_ROOT)
-    })
-  }
-
-  await app.register(serveFilePlugin)
-  await app.register(serveWebmanifest, { prefix: process.env.APP_ROOT })
-
-  return app
-}
diff --git a/src/files.js b/src/files.js
index 0ee22f7..5d852c4 100644
--- a/src/files.js
+++ b/src/files.js
@@ -1,11 +1,11 @@
-import { configMap } from './configMap.js'
-import { isJSFile } from './util.js'
-import { getCSSDependenciesFor, getViteManifests } from './manifests.js'
+import nodePath from 'node:path'
+import { promisify } from 'node:util'
+import zlib from 'node:zlib'
 import * as cache from './cache.js'
+import { configMap } from './config_map.js'
+import { NotFoundError, VersionMismatchError, isVersionMismatchError } from './errors.js'
 import logger from './logger.js'
-import { isVersionMismatchError, NotFoundError, VersionMismatchError } from './errors.js'
-import zlib from 'node:zlib'
-import { promisify } from 'node:util'
+import { getCSSDependenciesFor, getViteManifests } from './manifests.js'
 import { getVersionInfo, updateVersionProcessor } from './version.js'
 
 const gzip = promisify(zlib.gzip)
@@ -15,21 +15,10 @@ const compressFileSize = Number(process.env.COMPRESS_FILE_SIZE)
 const compressionMimeTypes = (process.env.COMPRESS_FILE_TYPES || '').replace(/([.+*?^$()[\]{}|])/g, '\\$1').split(' ')
 const compressionWhitelistRegex = new RegExp(`^(${compressionMimeTypes.join('|')})($|;)`, 'i')
 
-export function createWritable (body) {
-  if (typeof body !== 'string' && !(body instanceof Buffer)) return JSON.stringify(body)
-  return body
-}
-
-async function createFileBuffer (response, dependencies) {
-  const arrayBuffer = await response.arrayBuffer()
-  const buffer = Buffer.from(arrayBuffer)
-  return buffer
-}
-
 export async function fetchFileWithHeadersFromBaseUrl ({ path, baseUrl, version }) {
   const [response, dependencies] = await Promise.all([
     fetch(new URL(path, baseUrl), { cache: 'no-store' }),
-    isJSFile(path) && getCSSDependenciesFor({ file: path.substr(1), version })
+    nodePath.extname(path) === '.js' && getCSSDependenciesFor({ file: path.substr(1), version })
   ])
 
   if (!response.ok) {
@@ -47,7 +36,7 @@ export async function fetchFileWithHeadersFromBaseUrl ({ path, baseUrl, version
   }
 
   const result = {
-    body: await createFileBuffer(response, dependencies),
+    body: Buffer.from(await response.arrayBuffer()),
     headers: {
       'content-type': response.headers.get('content-type'),
       dependencies
diff --git a/src/handlers/version.js b/src/handlers/version.js
deleted file mode 100644
index d02e90f..0000000
--- a/src/handlers/version.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { getLatestVersion } from '../version.js'
-
-export default async function versionHandler (req, reply) {
-  const latestVersion = await getLatestVersion()
-  const version = req.headers.version || latestVersion
-  reply.header('version', version)
-  reply.header('latest-version', latestVersion)
-  reply.version = version
-}
diff --git a/src/index.js b/src/index.js
index 3b4b717..5ef2eea 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,32 +1,52 @@
 // Add env vars from files
 // Note: actual env vars supersede .env file and .env file supersedes .env.defaults file
 import { config } from 'dotenv-defaults'
+import { fileURLToPath } from 'node:url'
+import { dirname, join } from 'node:path'
+import { randomUUID } from 'node:crypto'
 import logger from './logger.js'
-import { createApp } from './create-app.js'
+
+import fastify from 'fastify'
+import autoLoad from '@fastify/autoload'
+
 import { getLatestVersion } from './version.js'
-import { configMap } from './configMap.js'
+import { configMap } from './config_map.js'
 import * as redis from './redis.js'
-import lightshipCjs from 'lightship'
-import { createMetricsServer } from './metrics.js'
 import { warmCache } from './files.js'
+import lightship from './lightship.js'
 
-config()
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = dirname(__filename)
 
-const { createLightship } = lightshipCjs
-const lightship = await createLightship()
+// Load env vars from .env and .env.defaults files
+// Note: actual env vars supersede .env file and .env file supersedes .env.defaults file
+config()
 
 lightship.queueBlockingTask(redis.isReady())
 lightship.queueBlockingTask(configMap.load())
 lightship.queueBlockingTask(getLatestVersion()
   .then(() => logger.info('[Health] Check latest version on startup.')))
 
-const app = await createApp()
-const metricsServer = await createMetricsServer()
-
-app.addHook('onReady', () => {
-  lightship.signalReady()
+// Create a Fastify server
+const app = fastify({
+  requestIdLogLabel: 'requestId',
+  disableRequestLogging: true,
+  logger,
+  connectionTimeout: 30000,
+  // genreqId is used to generate a request id if one is not provided by the proxy
+  genReqId: () => randomUUID()
+  // This is the name of the header used to pass a request id from a proxy to the service
+  // requestIdHeader: 'x-request-id',
 })
 
+// Register plugins
+// Note: plugins are loaded in alphabetical order
+app.register(autoLoad, { dir: join(__dirname, 'plugins') })
+
+// Register routes
+// Note: routes are loaded in alphabetical order
+app.register(autoLoad, { dir: join(__dirname, 'routes'), prefix: process.env.APP_ROOT, autoHooks: true })
+
 app.addHook('onReady', () => {
   // don't block the onReady hook
   getLatestVersion()
@@ -34,28 +54,23 @@ app.addHook('onReady', () => {
     .catch(err => logger.error(err))
 })
 
-// Binds and listens for connections on the specified host and port
-app.listen({ host: '::', port: Number(process.env.PORT) })
+// This hook is used to signal lightship that the service is ready to receive requests
+app.addHook('onReady', () => { lightship.signalReady() })
 
-lightship.registerShutdownHandler(async () => {
-  logger.info('[Health] Shutting down...')
-  if (redis.isEnabled()) {
-    await Promise.all([
-      redis.client.quit(),
-      redis.pubClient.quit(),
-      redis.subClient.quit()
-    ])
-  }
-  await app.close()
-  await metricsServer.close()
-})
-
-process.on('uncaughtException', async err => {
-  logger.error(err, 'uncaughtException')
+try {
+  // Binds and listens for connections on the specified host and port
+  await app.listen({ host: '::', port: Number(process.env.PORT) })
+} catch (err) {
+  logger.error(err)
   await lightship.shutdown()
-})
+}
 
-process.on('unhandledRejection', async err => {
-  logger.error(err, 'unhandledRejection')
-  await lightship.shutdown()
+lightship.registerShutdownHandler(async () => {
+  logger.info('[Service] Shutting down...')
+  await Promise.all([
+    redis.client.quit(),
+    redis.pubClient.quit(),
+    redis.subClient.quit()
+  ])
+  await app.close()
 })
diff --git a/src/lightship.js b/src/lightship.js
new file mode 100644
index 0000000..066054b
--- /dev/null
+++ b/src/lightship.js
@@ -0,0 +1,25 @@
+import lightshipCjs from 'lightship'
+import logger from './logger.js'
+
+const { createLightship } = lightshipCjs
+const lightship = await createLightship()
+
+// This is a graceful shutdown handler in case of uncaught exceptions
+process.on('uncaughtException', async err => {
+  logger.error(err, 'uncaughtException')
+  await lightship.shutdown()
+})
+
+// This is a graceful shutdown handler in case of unhandled promise rejections
+process.on('unhandledRejection', async err => {
+  logger.error(err, 'unhandledRejection')
+  await lightship.shutdown()
+})
+
+// This is a graceful shutdown handler in case of SIGINT
+process.on('SIGINT', async (process) => {
+  logger.info('SIGINT received')
+  await lightship.shutdown()
+})
+
+export default lightship
diff --git a/src/manifests.js b/src/manifests.js
index f1199ed..a7c5ba3 100644
--- a/src/manifests.js
+++ b/src/manifests.js
@@ -1,4 +1,4 @@
-import { configMap } from './configMap.js'
+import { configMap } from './config_map.js'
 import { getRedisKey, viteManifestToDeps, viteToOxManifest } from './util.js'
 import logger from './logger.js'
 import * as cache from './cache.js'
diff --git a/src/metrics.js b/src/metrics.js
deleted file mode 100644
index 221e0e0..0000000
--- a/src/metrics.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import fastify from 'fastify'
-import promClient from 'prom-client'
-
-const app = fastify({ logger: false })
-
-export const createMetricsServer = async () => {
-  await app.get('/metrics', async (request, reply) => {
-    reply
-      .type(promClient.register.contentType)
-      .send(await promClient.register.metrics())
-  })
-  await app.listen({ host: '::', port: Number(process.env.METRICS_PORT) })
-  return app
-}
diff --git a/src/plugins/cors.js b/src/plugins/cors.js
new file mode 100644
index 0000000..dc35145
--- /dev/null
+++ b/src/plugins/cors.js
@@ -0,0 +1,11 @@
+import cors from '@fastify/cors'
+import fp from 'fastify-plugin'
+const originsFromEnv = (process.env.ORIGINS || '').split(',')
+export const origin = originsFromEnv.length === 1 ? originsFromEnv[0] : originsFromEnv
+export default fp(async (fastify) => {
+  fastify.register(cors, {
+    origin,
+    methods: ['GET', 'POST'],
+    maxAge: 86400
+  })
+})
diff --git a/src/plugins/formbody.js b/src/plugins/formbody.js
new file mode 100644
index 0000000..8244bab
--- /dev/null
+++ b/src/plugins/formbody.js
@@ -0,0 +1,5 @@
+import formbody from '@fastify/formbody'
+import fp from 'fastify-plugin'
+export default fp(async (fastify) => {
+  fastify.register(formbody)
+})
diff --git a/src/plugins/helmet.js b/src/plugins/helmet.js
new file mode 100644
index 0000000..00c9f35
--- /dev/null
+++ b/src/plugins/helmet.js
@@ -0,0 +1,22 @@
+import helmet from '@fastify/helmet'
+import fp from 'fastify-plugin'
+
+/**
+ * @fastify/helmet enables the use of helmet in a Fastify application.
+ *
+ * Helmet helps you secure your Fastify apps by setting various HTTP headers.
+ * It's not a silver bullet, but it can help!
+ *
+ * Options can be set in the second parameter of the plugin registration.
+ *
+ * @see https://github.com/fastify/fastify-helmet
+ */
+
+export default fp(async (fastify) => {
+  fastify.register(helmet, {
+    contentSecurityPolicy: false,
+    crossOriginEmbedderPolicy: false,
+    originAgentCluster: false,
+    crossOriginOpenerPolicy: { policy: 'same-origin-allow-popups' }
+  })
+})
diff --git a/src/plugins/logging.js b/src/plugins/logging.js
new file mode 100644
index 0000000..87f4770
--- /dev/null
+++ b/src/plugins/logging.js
@@ -0,0 +1,24 @@
+import fp from 'fastify-plugin'
+
+// This plugin is used to log requests and responses.
+export default fp(async (fastify) => {
+  // Logs the request body with the 'trace' level
+  fastify.addHook('preHandler', function (req, reply, done) {
+    if (req.body) req.log.trace({ body: req.body }, 'parsed body')
+    done()
+  })
+
+  fastify.addHook('onError', (req, reply, { message, stack, statusCode }, done) => {
+    const error = { statusCode, stack }
+    reply.log.error(error, message || 'request errored')
+    done()
+  })
+
+  // Logs the request with the 'debug' level and also logs headers with the 'trace' level
+  fastify.addHook('onResponse', (req, reply, done) => {
+    const loggingOptions = { url: req.raw.url, res: reply, responseTime: reply.getResponseTime() }
+    if (process.env.LOG_LEVEL === 'trace') loggingOptions.headers = req.headers
+    reply.log.debug(loggingOptions, 'request completed')
+    done()
+  })
+})
diff --git a/src/plugins/metadata.js b/src/plugins/metadata.js
deleted file mode 100644
index 8191788..0000000
--- a/src/plugins/metadata.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { getMergedMetadata } from '../meta.js'
-
-export default async function metadataPlugin (fastify, options) {
-  fastify.get('/meta', async (req, res) => {
-    res.send(await getMergedMetadata({ version: res.version }))
-  })
-}
diff --git a/src/plugins/metrics.js b/src/plugins/metrics.js
new file mode 100644
index 0000000..7d13ae5
--- /dev/null
+++ b/src/plugins/metrics.js
@@ -0,0 +1,37 @@
+import fastify from 'fastify'
+import promClient from 'prom-client'
+import fp from 'fastify-plugin'
+import logger from '../logger.js'
+import lightship from '../lightship.js'
+
+import fastifyMetrics from 'fastify-metrics'
+
+// This is a metrics server that exposes metrics to Prometheus on the /metrics endpoint on the METRICS_PORT
+const app = fastify({ logger: false })
+await app.get('/metrics', async (request, reply) => {
+  reply
+    .type(promClient.register.contentType)
+    .send(await promClient.register.metrics())
+})
+try {
+  // Binds and listens for connections on the specified host and port
+  await app.listen({ host: '::', port: Number(process.env.METRICS_PORT) })
+  logger.info(`Metrics Server listening on port ${process.env.METRICS_PORT}`)
+} catch (err) {
+  logger.error(err)
+  await lightship.shutdown()
+}
+
+// This is a graceful shutdown handler for the metrics server
+lightship.registerShutdownHandler(async () => {
+  await logger.info('Metrics Server Shutting down...')
+  if (app) await app.close()
+})
+
+// This plugin is used to expose metrics to Prometheus.
+export default fp(async (fastify) => {
+  await fastify.register(fastifyMetrics, {
+    // do not expose metrics endpoint
+    endpoint: null
+  })
+})
diff --git a/src/plugins/sensible.js b/src/plugins/sensible.js
new file mode 100644
index 0000000..f31f320
--- /dev/null
+++ b/src/plugins/sensible.js
@@ -0,0 +1,5 @@
+import sensible from '@fastify/sensible'
+import fp from 'fastify-plugin'
+export default fp(async (fastify) => {
+  fastify.register(sensible)
+})
diff --git a/src/plugins/swagger.js b/src/plugins/swagger.js
new file mode 100644
index 0000000..81f31a2
--- /dev/null
+++ b/src/plugins/swagger.js
@@ -0,0 +1,38 @@
+import fp from 'fastify-plugin'
+import fastifySwaggerUi from '@fastify/swagger-ui'
+import fastifySwagger from '@fastify/swagger'
+import yaml from 'js-yaml'
+import fs from 'node:fs'
+
+const openapi = yaml.load(fs.readFileSync('./openapi.yaml', 'utf8'))
+
+const config = {
+  routePrefix: '/api-docs',
+  prefix: process.env.APP_ROOT,
+  openapi
+}
+
+// This plugin is used to expose the Swagger UI on the /api-docs endpoint
+export default fp(async (fastify) => {
+  if (process.env.EXPOSE_API_DOCS !== 'true') return
+
+  fastify.log.info('Registering Swagger UI on /api-docs endpoint. To disable this, set EXPOSE_API_DOCS=false.')
+
+  await fastify.register(fastifySwagger, config)
+
+  await fastify.register(fastifySwaggerUi, {
+    routePrefix: '/api-docs',
+    uiConfig: {
+      docExpansion: 'full',
+      deepLinking: false
+    },
+    uiHooks: {
+      onRequest: function (request, reply, next) { next() },
+      preHandler: function (request, reply, next) { next() }
+    },
+    staticCSP: true,
+    transformStaticCSP: (header) => header,
+    transformSpecification: (swaggerObject, request, reply) => { return swaggerObject },
+    transformSpecificationClone: true
+  })
+})
diff --git a/src/plugins/url-data.js b/src/plugins/url-data.js
new file mode 100644
index 0000000..b7dcceb
--- /dev/null
+++ b/src/plugins/url-data.js
@@ -0,0 +1,5 @@
+import urlData from '@fastify/url-data'
+import fp from 'fastify-plugin'
+export default fp(async (fastify) => {
+  fastify.register(urlData)
+})
diff --git a/src/redis.js b/src/redis.js
index 2127993..6c78ae5 100644
--- a/src/redis.js
+++ b/src/redis.js
@@ -6,27 +6,6 @@ import { registerLatestVersionListener, updateVersionProcessor } from './version
 const commonQueueOptions = { enableReadyCheck: false, maxRetriesPerRequest: null }
 
 const createClient = (type, options = {}) => {
-  if (!isEnabled()) {
-    return new Proxy({
-      getBuffer () {},
-      get () { return Promise.resolve() },
-      set () { return Promise.resolve() },
-      del () {},
-      flushdb () { return {} },
-      status: '',
-      duplicate () { return new Redis() },
-      publish () {},
-      subscribe () {},
-      on () {},
-      async ttl () { return -1 },
-      quit () { }
-    }, {
-      get () {
-        throw new Error('Redis is disabled. Check for redis.isEnabled()')
-      }
-    })
-  }
-
   const client = new Redis({
     host: process.env.REDIS_HOST,
     port: Number(process.env.REDIS_PORT),
@@ -42,7 +21,6 @@ const createClient = (type, options = {}) => {
 
 export async function isReady () {
   return new Promise(resolve => {
-    if (!isEnabled()) return resolve(true)
     client.on('ready', () => resolve(true))
     client.on('error', () => resolve(false))
   }).catch(() => false)
@@ -56,30 +34,24 @@ export const subClient = createClient('sub client', commonQueueOptions)
  * Bull specific things are below
  */
 
-if (isEnabled()) {
-  client.on('ready', () => {
-    const updateVersionQueue = getQueue('update-version')
-    try {
-      updateVersionQueue.process(updateVersionProcessor)
-      logger.debug('[Redis] Register version update processor.')
-    } catch (err) {
-      logger.debug('[Redis] Update version processor is already registered.')
-    }
-    updateVersionQueue.add({}, {
-      jobId: 'update-version-job',
-      repeat: { every: Number(process.env.CACHE_TTL) },
-      removeOnComplete: 10,
-      removeOnFail: 10,
-      timeout: 10000
-    })
+client.on('ready', () => {
+  const updateVersionQueue = getQueue('update-version')
+  try {
+    updateVersionQueue.process(updateVersionProcessor)
+    logger.debug('[Redis] Register version update processor.')
+  } catch (err) {
+    logger.debug('[Redis] Update version processor is already registered.')
+  }
+  updateVersionQueue.add({}, {
+    jobId: 'update-version-job',
+    repeat: { every: Number(process.env.CACHE_TTL) },
+    removeOnComplete: 10,
+    removeOnFail: 10,
+    timeout: 10000
   })
+})
 
-  registerLatestVersionListener(subClient)
-} else {
-  setTimeout(() => {
-    setInterval(updateVersionProcessor, Number(process.env.CACHE_TTL))
-  })
-}
+registerLatestVersionListener(subClient)
 
 /*
  * queue specific code
@@ -89,7 +61,6 @@ const queues = {}
 
 export function getQueue (name) {
   if (queues[name]) return queues[name]
-  // @ts-ignore
   return (queues[name] = new Queue(name, {
     prefix: process.env.REDIS_PREFIX,
     createClient: function (type) {
diff --git a/src/routes/autohooks.js b/src/routes/autohooks.js
new file mode 100644
index 0000000..41320ea
--- /dev/null
+++ b/src/routes/autohooks.js
@@ -0,0 +1,28 @@
+import { getLatestVersion } from '../version.js'
+export default async function (app, opts) {
+  // Add a hook to set the version headers
+  app.addHook('preHandler', async (req, reply) => {
+    const latestVersion = await getLatestVersion()
+    const version = req.headers.version || latestVersion
+    reply.header('version', version)
+    reply.header('latest-version', latestVersion)
+    reply.version = version
+  })
+
+  // Add a hook to log errors
+  app.addHook('onError', (req, reply, { message, stack, statusCode }, done) => {
+    const error = { statusCode, stack }
+    /* c8 ignore next */
+    reply.log.error(error, message || 'request errored')
+    done()
+  })
+
+  // Logs the request with the 'debug' level and also logs headers with the 'trace' level
+  app.addHook('onResponse', (req, reply, done) => {
+    const loggingOptions = { url: req.raw.url, res: reply, responseTime: reply.getResponseTime() }
+    /* c8 ignore next */
+    if (process.env.LOG_LEVEL === 'trace') loggingOptions.headers = req.headers
+    reply.log.debug(loggingOptions, 'request completed')
+    done()
+  })
+}
diff --git a/src/plugins/manifests.js b/src/routes/manifests.js
similarity index 71%
rename from src/plugins/manifests.js
rename to src/routes/manifests.js
index 5676ad2..21f5514 100644
--- a/src/plugins/manifests.js
+++ b/src/routes/manifests.js
@@ -1,9 +1,7 @@
 import { getOxManifests } from '../manifests.js'
 
-export default async function manifestsPlugin (fastify, options) {
+export default async function manifestsPlugin (fastify) {
   fastify.get('/manifests', async (req, reply) => {
-    if (reply.body) return
-
     const { body, headers } = await getOxManifests({ version: reply.version })
     reply.headers(headers)
     reply.send(body)
diff --git a/src/meta.js b/src/routes/metadata.js
similarity index 56%
rename from src/meta.js
rename to src/routes/metadata.js
index 17adfb2..bc81c21 100644
--- a/src/meta.js
+++ b/src/routes/metadata.js
@@ -1,8 +1,8 @@
-import { configMap } from './configMap.js'
-import * as cache from './cache.js'
-import { getRedisKey } from './util.js'
+import { configMap } from '../config_map.js'
+import * as cache from '../cache.js'
+import { getRedisKey } from '../util.js'
 
-export async function fetchMergedMetadata () {
+async function fetchMergedMetadata () {
   const metadata = await Promise.all(configMap.urls.map(async url => {
     const { origin } = new URL(url)
     try {
@@ -26,6 +26,9 @@ export async function fetchMergedMetadata () {
   return metadata.filter(Boolean)
 }
 
-export function getMergedMetadata ({ version }) {
-  return cache.get(getRedisKey({ version, name: 'mergedMetadata' }), async () => [await fetchMergedMetadata()])
+export default async function metadataPlugin (fastify) {
+  fastify.get('/meta', async (req, res) => {
+    const mergedMetadata = await cache.get(getRedisKey({ version: res.version, name: 'mergedMetadata' }), async () => [await fetchMergedMetadata()])
+    res.send(mergedMetadata)
+  })
 }
diff --git a/src/plugins/redirects.js b/src/routes/redirects.js
similarity index 50%
rename from src/plugins/redirects.js
rename to src/routes/redirects.js
index d057a2b..e2921c8 100644
--- a/src/plugins/redirects.js
+++ b/src/routes/redirects.js
@@ -1,4 +1,4 @@
-export default async function redirectsPlugin (fastify, options) {
+export default async function redirectsPlugin (fastify) {
   fastify.get('/ui', async (req, res) => {
     res.redirect(process.env.APP_ROOT)
   })
@@ -7,4 +7,10 @@ export default async function redirectsPlugin (fastify, options) {
     const location = req.body?.location || '../busy.html'
     res.redirect(location)
   })
+
+  if (process.env.APP_ROOT.length > 1) {
+    fastify.get(process.env.APP_ROOT.slice(0, -1), async (req, res) => {
+      res.redirect(process.env.APP_ROOT)
+    })
+  }
 }
diff --git a/src/plugins/serve-files.js b/src/routes/serve-files.js
similarity index 79%
rename from src/plugins/serve-files.js
rename to src/routes/serve-files.js
index aa5ba3e..f0f2a84 100644
--- a/src/plugins/serve-files.js
+++ b/src/routes/serve-files.js
@@ -1,6 +1,5 @@
 import { getFile } from '../files.js'
 import { isNotFoundError, isVersionMismatchError } from '../errors.js'
-import createError from 'http-errors'
 
 export default async function serveFilePlugin (fastify, options) {
   fastify.get('*', async (req, reply) => {
@@ -14,8 +13,8 @@ export default async function serveFilePlugin (fastify, options) {
       reply.headers(headers)
       reply.send(body)
     } catch (err) {
-      if (isNotFoundError(err) || isVersionMismatchError(err)) throw createError(404, `File "${req.urlData('path')}" does not exist.`)
-      throw createError(err.statusCode || 500)
+      if (isNotFoundError(err) || isVersionMismatchError(err)) throw fastify.httpErrors.createError(404, `File "${req.urlData('path')}" does not exist.`)
+      throw fastify.httpErrors.createError(err.statusCode || 500)
     }
   })
 }
diff --git a/src/plugins/webmanifest.js b/src/routes/webmanifest.js
similarity index 83%
rename from src/plugins/webmanifest.js
rename to src/routes/webmanifest.js
index 4951213..bbd13c4 100644
--- a/src/plugins/webmanifest.js
+++ b/src/routes/webmanifest.js
@@ -1,9 +1,18 @@
 import { get } from '../cache.js'
-import Validator from '../validator.js'
+import Ajv from 'ajv'
+import fs from 'node:fs'
 import { getRedisKey } from '../util.js'
 
 const appRoot = process.env.APP_ROOT
 
+const ajv = new Ajv({ allErrors: true })
+
+ajv.addSchema(
+  fs.readdirSync('src/schemas/', 'utf8').map(file => {
+    return JSON.parse(fs.readFileSync(`src/schemas/${file}`, 'utf8'))
+  })
+)
+
 const template = {
   // custom values
   name: 'OX App Suite',
@@ -32,39 +41,15 @@ const template = {
   ]
 }
 
-export default async function serveWebmanifest (fastify) {
-  fastify.get('/pwa.json', async (req, res) => {
-    const urlData = req.urlData()
-    const url = `https://${urlData.host}${urlData.path}`
-
-    try {
-      const cached = await get(getRedisKey({ name: `cachedManifest:${url}` }), async () => [await fetchWebManifest(url), 86400])
-      res.type('application/manifest+json')
-      res.send(cached)
-    } catch (err) {
-      res.statusCode = 500
-      res.send(`Failed to load config for url ${url}: ${err}`)
-    }
-  })
-}
-
-async function fetchWebManifest (url) {
-  const serverConfigURL = new URL('api/apps/manifests?action=config', url)
-  const conf = await fetch(serverConfigURL)
-
-  if (conf.ok) {
-    const data = (await conf.json()).data
-    if (String(data.pwa?.enabled) !== 'true') return {}
-
-    const combinedManifest = data.pwa.raw_manifest || Object.assign({}, template, buildManifestFromData(data.pwa))
-    const valid = Validator.validate('https://json.schemastore.org/web-manifest-combined.json', combinedManifest)
-    if (!valid) {
-      throw new Error(JSON.stringify(Validator.errors[0], null, 2))
-    }
-    const webmanifest = JSON.stringify(combinedManifest, null, 2)
-    return webmanifest
-  } else {
-    throw new Error(`Failed to fetch ${serverConfigURL}`)
+function getTypeFromPath (path) {
+  const ext = path.split('.').pop()
+  switch (ext) {
+    case 'png':
+      return 'image/png'
+    case 'svg':
+      return 'image/svg+xml'
+    default:
+      throw new Error('Unsupported file type or no file type.')
   }
 }
 
@@ -95,14 +80,38 @@ function buildManifestFromData (userData) {
   return manifestData
 }
 
-function getTypeFromPath (path) {
-  const ext = path.split('.').pop()
-  switch (ext) {
-    case 'png':
-      return 'image/png'
-    case 'svg':
-      return 'image/svg+xml'
-    default:
-      throw new Error('Unsupported file type or no file type.')
+async function fetchWebManifest (url) {
+  const serverConfigURL = new URL('api/apps/manifests?action=config', url)
+  const conf = await fetch(serverConfigURL)
+
+  if (conf.ok) {
+    const data = (await conf.json()).data
+    if (String(data.pwa?.enabled) !== 'true') return {}
+
+    const combinedManifest = data.pwa.raw_manifest || { ...template, ...buildManifestFromData(data.pwa) }
+    const valid = ajv.validate('https://json.schemastore.org/web-manifest-combined.json', combinedManifest)
+    if (!valid) {
+      throw new Error(JSON.stringify(ajv.errors[0], null, 2))
+    }
+    const webmanifest = JSON.stringify(combinedManifest, null, 2)
+    return webmanifest
+  } else {
+    throw new Error(`Failed to fetch ${serverConfigURL}`)
   }
 }
+
+export default async function serveWebmanifest (fastify) {
+  fastify.get('/pwa.json', async (req, res) => {
+    const urlData = req.urlData()
+    const url = `https://${urlData.host}${urlData.path}`
+
+    try {
+      const cached = await get(getRedisKey({ name: `cachedManifest:${url}` }), async () => [await fetchWebManifest(url), 86400])
+      res.type('application/manifest+json')
+      res.send(cached)
+    } catch (err) {
+      res.statusCode = 500
+      res.send(`Failed to load config for url ${url}: ${err}`)
+    }
+  })
+}
diff --git a/src/util.js b/src/util.js
index 70c7360..f9b1480 100644
--- a/src/util.js
+++ b/src/util.js
@@ -17,11 +17,6 @@ export function hash (array) {
   return new Uint32Array([hash]).toString()
 }
 
-export function isJSFile (name) {
-  const extname = path.extname(name)
-  return extname === '.js'
-}
-
 export function viteManifestToDeps (viteManifest) {
   const deps = {}
   for (const [codePoint, { isEntry, file, imports, css, assets }] of Object.entries(viteManifest)) {
@@ -60,18 +55,6 @@ export function getRedisKey ({ version = undefined, name }) {
   return `${process.env.REDIS_PREFIX}:${name}`
 }
 
-export function once (fn, context) {
-  let called = false
-  let res
-  return function () {
-    if (!called) {
-      res = fn.apply(context || this, arguments)
-      called = true
-    }
-    return res
-  }
-}
-
 export function asyncThrottle (fn, context) {
   let next = null
   let current = null
diff --git a/src/validator.js b/src/validator.js
deleted file mode 100644
index 54f4224..0000000
--- a/src/validator.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import Ajv from 'ajv'
-import fs from 'node:fs'
-
-const ajv = new Ajv({
-  allErrors: true
-})
-
-ajv.addSchema(
-  fs.readdirSync('src/schemas/', 'utf8').map(file => {
-    return JSON.parse(fs.readFileSync(`src/schemas/${file}`, 'utf8'))
-  })
-)
-
-export default ajv
diff --git a/src/version.js b/src/version.js
index 25a6b9e..3794fc3 100644
--- a/src/version.js
+++ b/src/version.js
@@ -1,4 +1,4 @@
-import { configMap } from './configMap.js'
+import { configMap } from './config_map.js'
 import { asyncThrottle, getRedisKey, hash } from './util.js'
 import logger from './logger.js'
 import * as cache from './cache.js'
@@ -64,17 +64,15 @@ export async function fetchVersionInfo () {
  */
 export async function getVersionInfo () {
   if (versionInfo.version) return versionInfo
-  if (redis.isEnabled()) {
-    const redisVersionInfo = await redis.client.get(getRedisKey({ name: 'versionInfo' }))
-    if (redisVersionInfo) {
-      try {
-        Object.assign(versionInfo, JSON.parse(redisVersionInfo))
-        logger.info(`[Version] Got initial version from redis: '${versionInfo.version}'`)
-        versionUpdateGauge.setToCurrentTime({ version: versionInfo.version })
-        return versionInfo
-      } catch (err) {
-        logger.error('[')
-      }
+  const redisVersionInfo = await redis.client.get(getRedisKey({ name: 'versionInfo' }))
+  if (redisVersionInfo) {
+    try {
+      Object.assign(versionInfo, JSON.parse(redisVersionInfo))
+      logger.info(`[Version] Got initial version from redis: '${versionInfo.version}'`)
+      versionUpdateGauge.setToCurrentTime({ version: versionInfo.version })
+      return versionInfo
+    } catch (err) {
+      logger.error('[')
     }
   }
 
@@ -83,11 +81,9 @@ export async function getVersionInfo () {
   logger.info(`[Version] Fetched initial version: '${fetchedVersionInfo.version}' - [${JSON.stringify(fetchedVersionInfo.details)}]`)
 
   Object.assign(versionInfo, fetchedVersionInfo)
-  if (redis.isEnabled()) {
-    const stringifiedVersionInfo = JSON.stringify(versionInfo)
-    redis.pubClient.publish(getRedisKey({ name: 'updateVersionInfo' }), stringifiedVersionInfo)
-    await redis.client.set(getRedisKey({ name: 'versionInfo' }), stringifiedVersionInfo)
-  }
+  const stringifiedVersionInfo = JSON.stringify(versionInfo)
+  redis.pubClient.publish(getRedisKey({ name: 'updateVersionInfo' }), stringifiedVersionInfo)
+  await redis.client.set(getRedisKey({ name: 'versionInfo' }), stringifiedVersionInfo)
 
   return versionInfo
 }
@@ -111,8 +107,6 @@ export async function getLatestVersion () {
 }
 
 export function registerLatestVersionListener (client) {
-  if (!redis.isEnabled()) return
-
   const key = getRedisKey({ name: 'updateVersionInfo' })
   client.subscribe(key, (errs, count) => logger.info(`[Redis] Subscribed to ${key}.`))
   client.on('message', async (channel, stringifiedVersionInfo) => {
@@ -148,29 +142,21 @@ export const updateVersionProcessor = asyncThrottle(async function updateVersion
       return storedVersion
     }
     logger.info(`[Version] Found new source version. Current version: '${storedVersion}', new version: '${fetchedVersionInfo.version}'`)
-    if (redis.isEnabled()) {
-      const prevProcessedVersion = await redis.client.get(getRedisKey({ name: 'prevProcessedVersion' }))
-      // that means, that between the previous update processing and this one, there was no version change
-      if (prevProcessedVersion === fetchedVersionInfo.version || immediate) {
-        logger.info('[Version] publish update to other nodes.')
-        // update local version info
-        Object.assign(versionInfo, fetchedVersionInfo)
-        const stringifiedVersionInfo = JSON.stringify(versionInfo)
-        redis.pubClient.publish(getRedisKey({ name: 'updateVersionInfo' }), stringifiedVersionInfo)
-        await redis.client.set(getRedisKey({ name: 'versionInfo' }), stringifiedVersionInfo)
-        versionUpdateGauge.setToCurrentTime({ version: versionInfo.version })
-      } else {
-        logger.info(`[Version] do not execute update yet. Store version ${fetchedVersionInfo.version} as previous version.`)
-        await redis.client.set(getRedisKey({ name: 'prevProcessedVersion' }), fetchedVersionInfo.version)
-      }
-    } else {
-      versionUpdateGauge.setToCurrentTime({ version: versionInfo.version })
-      // if redis is disabled, this will only be trigger by a setInterval and not from a redis event
-      logger.info('[Version] Clear local cache due to version update.')
-      cache.clear()
+    const prevProcessedVersion = await redis.client.get(getRedisKey({ name: 'prevProcessedVersion' }))
+    // that means, that between the previous update processing and this one, there was no version change
+    if (prevProcessedVersion === fetchedVersionInfo.version || immediate) {
+      logger.info('[Version] publish update to other nodes.')
+      // update local version info
       Object.assign(versionInfo, fetchedVersionInfo)
-      warmCache({ version: versionInfo.version }).catch(err => logger.error(err))
+      const stringifiedVersionInfo = JSON.stringify(versionInfo)
+      redis.pubClient.publish(getRedisKey({ name: 'updateVersionInfo' }), stringifiedVersionInfo)
+      await redis.client.set(getRedisKey({ name: 'versionInfo' }), stringifiedVersionInfo)
+      versionUpdateGauge.setToCurrentTime({ version: versionInfo.version })
+    } else {
+      logger.info(`[Version] do not execute update yet. Store version ${fetchedVersionInfo.version} as previous version.`)
+      await redis.client.set(getRedisKey({ name: 'prevProcessedVersion' }), fetchedVersionInfo.version)
     }
+
     return versionInfo.version
   } catch (err) {
     logger.error(`[Version] comparing version is not possible. Error: ${err.message}`)
diff --git a/yarn.lock b/yarn.lock
index 857b1dc..f5afffb 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,36 +2,26 @@
 # yarn lockfile v1
 
 
-"@assemblyscript/loader@^0.19.21":
-  version "0.19.23"
-  resolved "https://registry.yarnpkg.com/@assemblyscript/loader/-/loader-0.19.23.tgz#7fccae28d0a2692869f1d1219d36093bc24d5e72"
-  integrity sha512-ulkCYfFbYj01ie1MDOyxv2F6SpRN1TOj7fQxbP07D6HmeR+gr2JLSmINKjga2emB+b1L2KGrFKBTc+e00p54nw==
-
-"@colors/colors@1.5.0":
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
-  integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
-
 "@eslint-community/eslint-utils@^4.2.0":
-  version "4.3.0"
-  resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.3.0.tgz#a556790523a351b4e47e9d385f47265eaaf9780a"
-  integrity sha512-v3oplH6FYCULtFuCeqyuTd9D2WKO937Dxdq+GmHOLL72TTRriLxz2VLlNfkZRsvj6PKnOPAtuT6dwrs/pA5DvA==
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
+  integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==
   dependencies:
     eslint-visitor-keys "^3.3.0"
 
 "@eslint-community/regexpp@^4.4.0":
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.4.0.tgz#3e61c564fcd6b921cb789838631c5ee44df09403"
-  integrity sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ==
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.0.tgz#f6f729b02feee2c749f57e334b7a1b5f40a81724"
+  integrity sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==
 
-"@eslint/eslintrc@^2.0.1":
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.1.tgz#7888fe7ec8f21bc26d646dbd2c11cd776e21192d"
-  integrity sha512-eFRmABvW2E5Ho6f5fHLqgena46rOj7r7OKHYfLElqcBfGFHHpjBhivyi5+jOEQuSpdc/1phIZJlbC2te+tZNIw==
+"@eslint/eslintrc@^2.0.2":
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.2.tgz#01575e38707add677cf73ca1589abba8da899a02"
+  integrity sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==
   dependencies:
     ajv "^6.12.4"
     debug "^4.3.2"
-    espree "^9.5.0"
+    espree "^9.5.1"
     globals "^13.19.0"
     ignore "^5.2.0"
     import-fresh "^3.2.1"
@@ -39,10 +29,15 @@
     minimatch "^3.1.2"
     strip-json-comments "^3.1.1"
 
-"@eslint/js@8.36.0":
-  version "8.36.0"
-  resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.36.0.tgz#9837f768c03a1e4a30bd304a64fb8844f0e72efe"
-  integrity sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg==
+"@eslint/js@8.37.0":
+  version "8.37.0"
+  resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.37.0.tgz#cf1b5fa24217fe007f6487a26d765274925efa7d"
+  integrity sha512-x5vzdtOOGgFVDCUs81QRB2+liax8rFg3+7hqM+QhBG0/G3F1ZsoYl97UrqgHgQ9KKT7G6c4V+aTUCgu/n22v1A==
+
+"@fastify/accept-negotiator@^1.0.0":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz#c1c66b3b771c09742a54dd5bc87c582f6b0630ff"
+  integrity sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==
 
 "@fastify/ajv-compiler@^3.5.0":
   version "3.5.0"
@@ -53,6 +48,21 @@
     ajv-formats "^2.1.1"
     fast-uri "^2.0.0"
 
+"@fastify/autoload@^5.7.1":
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/@fastify/autoload/-/autoload-5.7.1.tgz#bd5fd2f496d3ef8c6f41bbab6666eae04bd260e6"
+  integrity sha512-F5c94MYAF0tacVu6X4/1ojO7fzmgrJXsqitDtpqknXgiHZpeFNhYSnNCUHPz6UDRKsfkDohmh0fiPTtOd8clzQ==
+  dependencies:
+    pkg-up "^3.1.0"
+
+"@fastify/cors@^8.2.1":
+  version "8.2.1"
+  resolved "https://registry.yarnpkg.com/@fastify/cors/-/cors-8.2.1.tgz#dd348162bcbfb87dff4b492e2bef32d41244006a"
+  integrity sha512-2H2MrDD3ea7g707g1CNNLWb9/tYbmw7HS+MK2SDcgjxwzbOFR93JortelTIO8DBFsZqFtEpKNxiZfSyrGgYcbw==
+  dependencies:
+    fastify-plugin "^4.0.0"
+    mnemonist "0.39.5"
+
 "@fastify/deepmerge@^1.0.0":
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/@fastify/deepmerge/-/deepmerge-1.3.0.tgz#8116858108f0c7d9fd460d05a7d637a13fe3239a"
@@ -86,6 +96,54 @@
     fastify-plugin "^4.2.1"
     helmet "^6.0.0"
 
+"@fastify/send@^2.0.0":
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/@fastify/send/-/send-2.0.1.tgz#db10d1401883b4aef41669fcf2ddb4e1bb4630df"
+  integrity sha512-8jdouu0o5d0FMq1+zCKeKXc1tmOQ5tTGYdQP3MpyF9+WWrZT1KCBdh6hvoEYxOm3oJG/akdE9BpehLiJgYRvGw==
+  dependencies:
+    "@lukeed/ms" "^2.0.1"
+    escape-html "~1.0.3"
+    fast-decode-uri-component "^1.0.1"
+    http-errors "2.0.0"
+    mime "^3.0.0"
+
+"@fastify/sensible@^5.2.0":
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/@fastify/sensible/-/sensible-5.2.0.tgz#fb346596597c2a2ebed6e747ac4beb6707843bae"
+  integrity sha512-fy5vqJJAMVQctUT+kYfGdaGYeW9d8JaWEqtdWN6UmzQQ3VX0qMXE7Qc/MmfE+Cj3v0g5kbeR+obd3HZr3IMf+w==
+  dependencies:
+    fast-deep-equal "^3.1.1"
+    fastify-plugin "^4.0.0"
+    forwarded "^0.2.0"
+    http-errors "^2.0.0"
+    ms "^2.1.3"
+    type-is "^1.6.18"
+    vary "^1.1.2"
+
+"@fastify/static@^6.0.0":
+  version "6.9.0"
+  resolved "https://registry.yarnpkg.com/@fastify/static/-/static-6.9.0.tgz#038efdfe33757cc0ab4b0920e82bc4240fa5d78a"
+  integrity sha512-9SBVNJi2+KTnfiW1WjiVXDsmUxliNI54OF1eOiaop264dh8FwXSuLmO62JXvx7+VD0vQXEqsyRbFCYUJ9aJxng==
+  dependencies:
+    "@fastify/accept-negotiator" "^1.0.0"
+    "@fastify/send" "^2.0.0"
+    content-disposition "^0.5.3"
+    fastify-plugin "^4.0.0"
+    glob "^8.0.1"
+    p-limit "^3.1.0"
+    readable-stream "^4.0.0"
+
+"@fastify/swagger-ui@^1.5.0":
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/@fastify/swagger-ui/-/swagger-ui-1.6.0.tgz#508b69d3ee75d8bfb7d05e572112c47c10f188d7"
+  integrity sha512-CfmRF79Ilrvo8k2M8hL6FrdOVTZyPa6UrEkN7n852CXPE+rqoxlN1eRrUg3qFkA93Wl6xEc1evAhFGz84uyM8A==
+  dependencies:
+    "@fastify/static" "^6.0.0"
+    fastify-plugin "^4.0.0"
+    openapi-types "^12.0.2"
+    rfdc "^1.3.0"
+    yaml "^2.1.3"
+
 "@fastify/swagger@^8.3.1":
   version "8.3.1"
   resolved "https://registry.yarnpkg.com/@fastify/swagger/-/swagger-8.3.1.tgz#4ea955723dccd4c4ec43d8431711f4286bfc3b47"
@@ -134,6 +192,11 @@
   resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11"
   integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==
 
+"@lukeed/ms@^2.0.1":
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/@lukeed/ms/-/ms-2.0.1.tgz#3c2bbc258affd9cc0e0cc7828477383c73afa6ee"
+  integrity sha512-Xs/4RZltsAL7pkvaNStUQt7netTkyxrS0K+RILcVr3TRMS/ToOg4I6uNfhB9SlGsnWBym4U+EaXq0f0cEMNkHA==
+
 "@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.2":
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz#44d752c1a2dc113f15f781b7cc4f53a307e3fa38"
@@ -201,15 +264,6 @@
     husky ">=8"
     lint-staged ">=13"
 
-"@open-xchange/logging@^0.1.6":
-  version "0.1.6"
-  resolved "https://registry.yarnpkg.com/@open-xchange/logging/-/logging-0.1.6.tgz#2bae6bbd83e222efc8d7fad8dcef9163396e99ef"
-  integrity sha512-tORIAJ6YxHlKVJ9XVXj55tb5iW5/vAiwQo9crFVHa8/L0iWgwaQRb4O21UuXJkauJyRx6r1q0FDYGJcSYD72yQ==
-  dependencies:
-    pino "^8.6.1"
-    pino-http "^8.2.1"
-    pino-pretty "^9.1.1"
-
 "@sentry/core@6.19.7":
   version "6.19.7"
   resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.19.7.tgz#156aaa56dd7fad8c89c145be6ad7a4f7209f9785"
@@ -301,16 +355,18 @@
   resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz#5981a8db18b56ba38ef0efb7d995b12aa7b51918"
   integrity sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==
 
+"@types/ioredis-mock@^8.2.1":
+  version "8.2.1"
+  resolved "https://registry.yarnpkg.com/@types/ioredis-mock/-/ioredis-mock-8.2.1.tgz#841af6b16fdcd2744c0b53363beeeb0b2aecb1d0"
+  integrity sha512-VZsxDuzie7RGiPn/eMQ6ODZd6Q2gb0fTkD2gyPKVkojRw4D1hPisQ87aD+cCHqGylKeU4x01VwQ4QA82Fx4fRg==
+  dependencies:
+    ioredis ">=5"
+
 "@types/json5@^0.0.29":
   version "0.0.29"
   resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
   integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
 
-abbrev@1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
-  integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
-
 abort-controller@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
@@ -466,11 +522,6 @@ array.prototype.flatmap@^1.3.1:
     es-abstract "^1.20.4"
     es-shim-unscopables "^1.0.0"
 
-asap@^2.0.0:
-  version "2.0.6"
-  resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
-  integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==
-
 assertion-error@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
@@ -481,45 +532,11 @@ astral-regex@^2.0.0:
   resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
   integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
 
-asynckit@^0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
-  integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
-
 atomic-sleep@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b"
   integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==
 
-autocannon@^7.10.0:
-  version "7.10.0"
-  resolved "https://registry.yarnpkg.com/autocannon/-/autocannon-7.10.0.tgz#2a048cd065669222474b6a0f9522f108bc93d50b"
-  integrity sha512-PY1UrXL4NHE7J0hA6GGN2r8xjiAePS/bii3Hz7NOvp4JO3xDNBgRftDjfAxj1t6FDWXiXEOuKF/pdDiisIS8ZA==
-  dependencies:
-    chalk "^4.1.0"
-    char-spinner "^1.0.1"
-    cli-table3 "^0.6.0"
-    color-support "^1.1.1"
-    cross-argv "^2.0.0"
-    form-data "^4.0.0"
-    has-async-hooks "^1.0.0"
-    hdr-histogram-js "^3.0.0"
-    hdr-histogram-percentiles-obj "^3.0.0"
-    http-parser-js "^0.5.2"
-    hyperid "^3.0.0"
-    lodash.chunk "^4.2.0"
-    lodash.clonedeep "^4.5.0"
-    lodash.flatten "^4.4.0"
-    manage-path "^2.0.0"
-    on-net-listen "^1.1.1"
-    pretty-bytes "^5.4.1"
-    progress "^2.0.3"
-    reinterval "^1.1.0"
-    retimer "^3.0.0"
-    semver "^7.3.2"
-    subarg "^1.0.0"
-    timestring "^6.0.0"
-
 available-typed-arrays@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
@@ -539,7 +556,7 @@ balanced-match@^1.0.0:
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
   integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
 
-base64-js@^1.2.0, base64-js@^1.3.1:
+base64-js@^1.3.1:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
   integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
@@ -659,17 +676,17 @@ chalk@^4.0.0, chalk@^4.1.0:
     ansi-styles "^4.1.0"
     supports-color "^7.1.0"
 
-char-spinner@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/char-spinner/-/char-spinner-1.0.1.tgz#e6ea67bd247e107112983b7ab0479ed362800081"
-  integrity sha512-acv43vqJ0+N0rD+Uw3pDHSxP30FHrywu2NO6/wBaHChJIizpDeBUd6NjqhNhy9LGaEAhZAXn46QzmlAvIWd16g==
+charenc@0.0.2:
+  version "0.0.2"
+  resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
+  integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==
 
 check-error@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
   integrity sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==
 
-chokidar@3.5.3, chokidar@^3.5.2:
+chokidar@3.5.3:
   version "3.5.3"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
   integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
@@ -696,15 +713,6 @@ cli-cursor@^3.1.0:
   dependencies:
     restore-cursor "^3.1.0"
 
-cli-table3@^0.6.0:
-  version "0.6.3"
-  resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.3.tgz#61ab765aac156b52f222954ffc607a6f01dbeeb2"
-  integrity sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==
-  dependencies:
-    string-width "^4.2.0"
-  optionalDependencies:
-    "@colors/colors" "1.5.0"
-
 cli-truncate@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7"
@@ -747,38 +755,28 @@ color-name@~1.1.4:
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
-color-support@^1.1.1:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
-  integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==
-
-colorette@^2.0.19, colorette@^2.0.7:
+colorette@^2.0.19:
   version "2.0.19"
   resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798"
   integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==
 
-combined-stream@^1.0.8:
-  version "1.0.8"
-  resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
-  integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
-  dependencies:
-    delayed-stream "~1.0.0"
-
 commander@^10.0.0:
   version "10.0.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.0.tgz#71797971162cd3cf65f0b9d24eb28f8d303acdf1"
   integrity sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==
 
-component-emitter@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
-  integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
-
 concat-map@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
   integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
 
+content-disposition@^0.5.3:
+  version "0.5.4"
+  resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
+  integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
+  dependencies:
+    safe-buffer "5.2.1"
+
 cookie@^0.4.1:
   version "0.4.2"
   resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432"
@@ -789,11 +787,6 @@ cookie@^0.5.0:
   resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b"
   integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
 
-cookiejar@^2.1.4:
-  version "2.1.4"
-  resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b"
-  integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==
-
 cron-parser@^4.2.1:
   version "4.8.1"
   resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.8.1.tgz#47062ea63d21d78c10ddedb08ea4c5b6fc2750fb"
@@ -801,11 +794,6 @@ cron-parser@^4.2.1:
   dependencies:
     luxon "^3.2.1"
 
-cross-argv@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/cross-argv/-/cross-argv-2.0.0.tgz#2e7907ba3246f82c967623a3e8525925bbd6c0ad"
-  integrity sha512-YIaY9TR5Nxeb8SMdtrU8asWVM4jqJDNDYlKV21LxtYcfNJhp1kEsgSa6qXwXgzN0WQWGODps0+TlGp2xQSHwOg==
-
 cross-spawn@^7.0.2, cross-spawn@^7.0.3:
   version "7.0.3"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@@ -815,10 +803,10 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3:
     shebang-command "^2.0.0"
     which "^2.0.1"
 
-dateformat@^4.6.3:
-  version "4.6.3"
-  resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5"
-  integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==
+crypt@0.0.2:
+  version "0.0.2"
+  resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
+  integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==
 
 debug@4, debug@4.3.4, debug@^4.0.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
   version "4.3.4"
@@ -874,11 +862,6 @@ delay@^5.0.0:
   resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d"
   integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==
 
-delayed-stream@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
-  integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
-
 denque@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1"
@@ -889,14 +872,6 @@ depd@2.0.0:
   resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
   integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
 
-dezalgo@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81"
-  integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==
-  dependencies:
-    asap "^2.0.0"
-    wrappy "1"
-
 diff@5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b"
@@ -948,13 +923,6 @@ emoji-regex@^9.2.2:
   resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
   integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
 
-end-of-stream@^1.1.0:
-  version "1.4.4"
-  resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
-  integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
-  dependencies:
-    once "^1.4.0"
-
 es-abstract@^1.19.0, es-abstract@^1.20.4:
   version "1.21.2"
   resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.21.2.tgz#a56b9695322c8a185dc25975aa3b8ec31d0e7eff"
@@ -1025,6 +993,11 @@ escalade@^3.1.1:
   resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
   integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
 
+escape-html@~1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+  integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
+
 escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
@@ -1100,9 +1073,9 @@ eslint-plugin-license-header@^0.6.0:
     requireindex "^1.2.0"
 
 eslint-plugin-n@^15.6.0:
-  version "15.6.1"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-n/-/eslint-plugin-n-15.6.1.tgz#f7e77f24abb92a550115cf11e29695da122c398c"
-  integrity sha512-R9xw9OtCRxxaxaszTQmQAlPgM+RdGjaL1akWuY/Fv9fRAi8Wj4CUKc6iYVG8QNRjRuo8/BqVYIpfqberJUEacA==
+  version "15.7.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-n/-/eslint-plugin-n-15.7.0.tgz#e29221d8f5174f84d18f2eb94765f2eeea033b90"
+  integrity sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==
   dependencies:
     builtins "^5.0.1"
     eslint-plugin-es "^4.1.0"
@@ -1150,20 +1123,20 @@ eslint-visitor-keys@^2.0.0:
   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303"
   integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
 
-eslint-visitor-keys@^3.3.0:
-  version "3.3.0"
-  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826"
-  integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
+eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.0:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz#c7f0f956124ce677047ddbc192a68f999454dedc"
+  integrity sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==
 
 eslint@^8.29.0:
-  version "8.36.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.36.0.tgz#1bd72202200a5492f91803b113fb8a83b11285cf"
-  integrity sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw==
+  version "8.37.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.37.0.tgz#1f660ef2ce49a0bfdec0b0d698e0b8b627287412"
+  integrity sha512-NU3Ps9nI05GUoVMxcZx1J8CNR6xOvUT4jAUMH5+z8lpp3aEdPVCImKw6PWG4PY+Vfkpr+jvMpxs/qoE7wq0sPw==
   dependencies:
     "@eslint-community/eslint-utils" "^4.2.0"
     "@eslint-community/regexpp" "^4.4.0"
-    "@eslint/eslintrc" "^2.0.1"
-    "@eslint/js" "8.36.0"
+    "@eslint/eslintrc" "^2.0.2"
+    "@eslint/js" "8.37.0"
     "@humanwhocodes/config-array" "^0.11.8"
     "@humanwhocodes/module-importer" "^1.0.1"
     "@nodelib/fs.walk" "^1.2.8"
@@ -1174,8 +1147,8 @@ eslint@^8.29.0:
     doctrine "^3.0.0"
     escape-string-regexp "^4.0.0"
     eslint-scope "^7.1.1"
-    eslint-visitor-keys "^3.3.0"
-    espree "^9.5.0"
+    eslint-visitor-keys "^3.4.0"
+    espree "^9.5.1"
     esquery "^1.4.2"
     esutils "^2.0.2"
     fast-deep-equal "^3.1.3"
@@ -1201,14 +1174,14 @@ eslint@^8.29.0:
     strip-json-comments "^3.1.0"
     text-table "^0.2.0"
 
-espree@^9.5.0:
-  version "9.5.0"
-  resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.0.tgz#3646d4e3f58907464edba852fa047e6a27bdf113"
-  integrity sha512-JPbJGhKc47++oo4JkEoTe2wjy4fmMwvFpgJT9cQzmfXKp22Dr6Hf1tdCteLz1h0P3t+mGvWZ+4Uankvh8+c6zw==
+espree@^9.5.1:
+  version "9.5.1"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.1.tgz#4f26a4d5f18905bf4f2e0bd99002aab807e96dd4"
+  integrity sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==
   dependencies:
     acorn "^8.8.0"
     acorn-jsx "^5.3.2"
-    eslint-visitor-keys "^3.3.0"
+    eslint-visitor-keys "^3.4.0"
 
 esquery@^1.4.2:
   version "1.5.0"
@@ -1264,11 +1237,6 @@ fast-content-type-parse@^1.0.0:
   resolved "https://registry.yarnpkg.com/fast-content-type-parse/-/fast-content-type-parse-1.0.0.tgz#cddce00df7d7efb3727d375a598e4904bfcb751c"
   integrity sha512-Xbc4XcysUXcsP5aHUU7Nq3OwvHq97C+WnbkeIefpeYLX+ryzFJlU6OStFJhs6Ol0LkUGpcK+wL0JwfM+FCU5IA==
 
-fast-copy@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.1.tgz#9e89ef498b8c04c1cd76b33b8e14271658a732aa"
-  integrity sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA==
-
 fast-decode-uri-component@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz#46f8b6c22b30ff7a81357d4f59abfae938202543"
@@ -1330,11 +1298,6 @@ fast-redact@^3.1.1:
   resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.2.tgz#d58e69e9084ce9fa4c1a6fa98a3e1ecf5d7839aa"
   integrity sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==
 
-fast-safe-stringify@^2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
-  integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
-
 fast-uri@^2.0.0, fast-uri@^2.1.0, fast-uri@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-2.2.0.tgz#519a0f849bef714aad10e9753d69d8f758f7445a"
@@ -1426,6 +1389,13 @@ find-up@5.0.0, find-up@^5.0.0:
     locate-path "^6.0.0"
     path-exists "^4.0.0"
 
+find-up@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
+  integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
+  dependencies:
+    locate-path "^3.0.0"
+
 flat-cache@^3.0.4:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
@@ -1451,26 +1421,7 @@ for-each@^0.3.3:
   dependencies:
     is-callable "^1.1.3"
 
-form-data@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
-  integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
-  dependencies:
-    asynckit "^0.4.0"
-    combined-stream "^1.0.8"
-    mime-types "^2.1.12"
-
-formidable@^2.1.2:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.1.2.tgz#fa973a2bec150e4ce7cac15589d7a25fc30ebd89"
-  integrity sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==
-  dependencies:
-    dezalgo "^1.0.4"
-    hexoid "^1.0.0"
-    once "^1.4.0"
-    qs "^6.11.0"
-
-forwarded@0.2.0:
+forwarded@0.2.0, forwarded@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
   integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
@@ -1580,7 +1531,7 @@ glob@^7.1.3:
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
-glob@^8.0.0:
+glob@^8.0.1:
   version "8.1.0"
   resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e"
   integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==
@@ -1617,21 +1568,11 @@ grapheme-splitter@^1.0.4:
   resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
   integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
 
-has-async-hooks@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/has-async-hooks/-/has-async-hooks-1.0.0.tgz#3df965ade8cd2d9dbfdacfbca3e0a5152baaf204"
-  integrity sha512-YF0VPGjkxr7AyyQQNykX8zK4PvtEDsUJAPqwu06UFz1lb6EvI53sPh5H1kWxg8NXI5LsfRCZ8uX9NkYDZBb/mw==
-
 has-bigints@^1.0.1, has-bigints@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
   integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==
 
-has-flag@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
-  integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==
-
 has-flag@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
@@ -1668,20 +1609,6 @@ has@^1.0.3:
   dependencies:
     function-bind "^1.1.1"
 
-hdr-histogram-js@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/hdr-histogram-js/-/hdr-histogram-js-3.0.0.tgz#8e2d9a68e3313147804c47d85a9c22a93f85e24b"
-  integrity sha512-/EpvQI2/Z98mNFYEnlqJ8Ogful8OpArLG/6Tf2bPnkutBVLIeMVNHjk1ZDfshF2BUweipzbk+dB1hgSB7SIakw==
-  dependencies:
-    "@assemblyscript/loader" "^0.19.21"
-    base64-js "^1.2.0"
-    pako "^1.0.3"
-
-hdr-histogram-percentiles-obj@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz#9409f4de0c2dda78e61de2d9d78b1e9f3cba283c"
-  integrity sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==
-
 he@1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
@@ -1692,20 +1619,7 @@ helmet@^6.0.0:
   resolved "https://registry.yarnpkg.com/helmet/-/helmet-6.0.1.tgz#52ec353638b2e87f14fe079d142b368ac11e79a4"
   integrity sha512-8wo+VdQhTMVBMCITYZaGTbE4lvlthelPYSvoyNvk4RECTmrVjMerp9RfUOQXZWLvCcAn1pKj7ZRxK4lI9Alrcw==
 
-help-me@^4.0.1:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/help-me/-/help-me-4.2.0.tgz#50712bfd799ff1854ae1d312c36eafcea85b0563"
-  integrity sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==
-  dependencies:
-    glob "^8.0.0"
-    readable-stream "^3.6.0"
-
-hexoid@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18"
-  integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==
-
-http-errors@^2.0.0:
+http-errors@2.0.0, http-errors@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
   integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
@@ -1716,11 +1630,6 @@ http-errors@^2.0.0:
     statuses "2.0.1"
     toidentifier "1.0.1"
 
-http-parser-js@^0.5.2:
-  version "0.5.8"
-  resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3"
-  integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==
-
 https-proxy-agent@^5.0.0:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
@@ -1739,24 +1648,11 @@ husky@>=8:
   resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184"
   integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==
 
-hyperid@^3.0.0:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/hyperid/-/hyperid-3.1.1.tgz#50fe8a75ff3ada74dacaf5a3761fb031bdf541c7"
-  integrity sha512-RveV33kIksycSf7HLkq1sHB5wW0OwuX8ot8MYnY++gaaPXGFfKpBncHrAWxdpuEeRlazUMGWefwP1w6o6GaumA==
-  dependencies:
-    uuid "^8.3.2"
-    uuid-parse "^1.1.0"
-
 ieee754@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
   integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
 
-ignore-by-default@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
-  integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==
-
 ignore@^5.1.1, ignore@^5.2.0:
   version "5.2.4"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
@@ -1788,7 +1684,7 @@ inflight@^1.0.4:
     once "^1.3.0"
     wrappy "1"
 
-inherits@2, inherits@2.0.4, inherits@^2.0.3:
+inherits@2, inherits@2.0.4:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -1803,16 +1699,17 @@ internal-slot@^1.0.5:
     side-channel "^1.0.4"
 
 ioredis-mock@^8.2.3:
-  version "8.2.6"
-  resolved "https://registry.yarnpkg.com/ioredis-mock/-/ioredis-mock-8.2.6.tgz#66af2165e39dd957dedfbc44118fc3d323a59610"
-  integrity sha512-Dc4fWVicryn1O6ZLT77TgKTATVBnk4XpUAYMQetKTLlKs4j7+/+6SUTYy0xnsQHVJedR0RBP3nCeqePobU/kog==
+  version "8.4.0"
+  resolved "https://registry.yarnpkg.com/ioredis-mock/-/ioredis-mock-8.4.0.tgz#872d7ab1b8ee210094a677f0c1334815f09df775"
+  integrity sha512-ZB+Wj9kzYbYcPrU2Xr61Fo+Fcc8Y1/sAnc/8sCKhyi69C4lQf7cdTEqiqRwneICX2OwgGtpCw8Udr7GEyZixOQ==
   dependencies:
     "@ioredis/as-callback" "^3.0.0"
     "@ioredis/commands" "^1.2.0"
     fengari "^0.1.4"
     fengari-interop "^0.1.3"
+    semver "^7.3.8"
 
-ioredis@^5.0.0, ioredis@^5.3.1:
+ioredis@>=5, ioredis@^5.0.0, ioredis@^5.3.1:
   version "5.3.1"
   resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.3.1.tgz#55d394a51258cee3af9e96c21c863b1a97bf951f"
   integrity sha512-C+IBcMysM6v52pTLItYMeV4Hz7uriGtoJdz7SSBDX6u+zwSYGirLdQh3L7t/OItWITcw3gTFMjJReYUwS4zihg==
@@ -1863,6 +1760,11 @@ is-boolean-object@^1.1.0:
     call-bind "^1.0.2"
     has-tostringtag "^1.0.0"
 
+is-buffer@~1.1.6:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+  integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
+
 is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7:
   version "1.2.7"
   resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055"
@@ -1961,7 +1863,7 @@ is-stream@^3.0.0:
   resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac"
   integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==
 
-is-string@^1.0.5, is-string@^1.0.7:
+is-string@^1.0.4, is-string@^1.0.5, is-string@^1.0.7:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd"
   integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==
@@ -2008,11 +1910,6 @@ isexe@^2.0.0:
   resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
   integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
 
-joycon@^3.1.1:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03"
-  integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==
-
 js-sdsl@^4.1.4:
   version "4.4.0"
   resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.0.tgz#8b437dbe642daa95760400b602378ed8ffea8430"
@@ -2127,6 +2024,14 @@ listr2@^5.0.7:
     through "^2.3.8"
     wrap-ansi "^7.0.0"
 
+locate-path@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
+  integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
+  dependencies:
+    p-locate "^3.0.0"
+    path-exists "^3.0.0"
+
 locate-path@^6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
@@ -2134,26 +2039,11 @@ locate-path@^6.0.0:
   dependencies:
     p-locate "^5.0.0"
 
-lodash.chunk@^4.2.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/lodash.chunk/-/lodash.chunk-4.2.0.tgz#66e5ce1f76ed27b4303d8c6512e8d1216e8106bc"
-  integrity sha512-ZzydJKfUHJwHa+hF5X66zLFCBrWn5GeF28OHEr4WVWtNDXlQ/IjWKPBiikqKo2ne0+v6JgCgJ0GzJp8k8bHC7w==
-
-lodash.clonedeep@^4.5.0:
-  version "4.5.0"
-  resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
-  integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==
-
 lodash.defaults@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
   integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==
 
-lodash.flatten@^4.4.0:
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
-  integrity sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==
-
 lodash.get@^4.4.2:
   version "4.4.2"
   resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
@@ -2169,6 +2059,11 @@ lodash.merge@^4.6.2:
   resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
   integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
 
+lodash.once@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
+  integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==
+
 lodash@^4.17.21:
   version "4.17.21"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
@@ -2216,21 +2111,25 @@ luxon@^3.2.1:
   resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.3.0.tgz#d73ab5b5d2b49a461c47cedbc7e73309b4805b48"
   integrity sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==
 
-manage-path@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/manage-path/-/manage-path-2.0.0.tgz#f4cf8457b926eeee2a83b173501414bc76eb9597"
-  integrity sha512-NJhyB+PJYTpxhxZJ3lecIGgh4kwIY2RAh44XvAz9UlqthlQwtPBf62uBVR8XaD8CRuSjQ6TnZH2lNJkbLPZM2A==
+md5@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f"
+  integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==
+  dependencies:
+    charenc "0.0.2"
+    crypt "0.0.2"
+    is-buffer "~1.1.6"
+
+media-typer@0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
+  integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
 
 merge-stream@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
   integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
 
-methods@^1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
-  integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
-
 micromatch@^4.0.5:
   version "4.0.5"
   resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
@@ -2244,17 +2143,17 @@ mime-db@1.52.0:
   resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
   integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
 
-mime-types@^2.1.12:
+mime-types@~2.1.24:
   version "2.1.35"
   resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
   integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
   dependencies:
     mime-db "1.52.0"
 
-mime@2.6.0:
-  version "2.6.0"
-  resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367"
-  integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==
+mime@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7"
+  integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==
 
 mimic-fn@^2.1.0:
   version "2.1.0"
@@ -2287,11 +2186,45 @@ minimatch@^5.0.1:
   dependencies:
     brace-expansion "^2.0.1"
 
-minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.6:
+minimist@^1.2.0, minimist@^1.2.6:
   version "1.2.8"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
   integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
 
+mkdirp@^1.0.4, mkdirp@~1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
+  integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
+
+mnemonist@0.39.5:
+  version "0.39.5"
+  resolved "https://registry.yarnpkg.com/mnemonist/-/mnemonist-0.39.5.tgz#5850d9b30d1b2bc57cc8787e5caa40f6c3420477"
+  integrity sha512-FPUtkhtJ0efmEFGpU14x7jGbTB+s18LrzRL2KgoWz9YvcY3cPomz8tih01GbHwnGk/OmkOKfqd/RAQoc8Lm7DQ==
+  dependencies:
+    obliterator "^2.0.1"
+
+mocha-junit-reporter@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-2.2.0.tgz#2663aaf25a98989ac9080c92b19e54209e539f67"
+  integrity sha512-W83Ddf94nfLiTBl24aS8IVyFvO8aRDLlCvb+cKb/VEaN5dEbcqu3CXiTe8MQK2DvzS7oKE1RsFTxzN302GGbDQ==
+  dependencies:
+    debug "^4.3.4"
+    md5 "^2.3.0"
+    mkdirp "~1.0.4"
+    strip-ansi "^6.0.1"
+    xml "^1.0.1"
+
+mocha-multi@^1.1.7:
+  version "1.1.7"
+  resolved "https://registry.yarnpkg.com/mocha-multi/-/mocha-multi-1.1.7.tgz#0f94d00c22ae39d20c253545d8eee0d2c4420205"
+  integrity sha512-SXZRgHy0XiRTASyOp0p6fjOkdj+R62L6cqutnYyQOvIjNznJuUwzykxctypeRiOwPd+gfn4yt3NRulMQyI8Tzg==
+  dependencies:
+    debug "^4.1.1"
+    is-string "^1.0.4"
+    lodash.once "^4.1.1"
+    mkdirp "^1.0.4"
+    object-assign "^4.1.1"
+
 mocha@^10.2.0:
   version "10.2.0"
   resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.2.0.tgz#1fd4a7c32ba5ac372e03a17eef435bd00e5c68b8"
@@ -2324,7 +2257,7 @@ ms@2.1.2:
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
   integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
 
-ms@2.1.3, ms@^2.1.1:
+ms@2.1.3, ms@^2.1.1, ms@^2.1.3:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
   integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
@@ -2376,29 +2309,6 @@ node-gyp-build-optional-packages@5.0.7:
   resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz#5d2632bbde0ab2f6e22f1bbac2199b07244ae0b3"
   integrity sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==
 
-nodemon@^2.0.22:
-  version "2.0.22"
-  resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.22.tgz#182c45c3a78da486f673d6c1702e00728daf5258"
-  integrity sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==
-  dependencies:
-    chokidar "^3.5.2"
-    debug "^3.2.7"
-    ignore-by-default "^1.0.1"
-    minimatch "^3.1.2"
-    pstree.remy "^1.1.8"
-    semver "^5.7.1"
-    simple-update-notifier "^1.0.7"
-    supports-color "^5.5.0"
-    touch "^3.1.0"
-    undefsafe "^2.0.5"
-
-nopt@~1.0.10:
-  version "1.0.10"
-  resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee"
-  integrity sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==
-  dependencies:
-    abbrev "1"
-
 normalize-path@^3.0.0, normalize-path@~3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
@@ -2411,6 +2321,11 @@ npm-run-path@^5.1.0:
   dependencies:
     path-key "^4.0.0"
 
+object-assign@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+  integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
+
 object-inspect@^1.12.3, object-inspect@^1.9.0:
   version "1.12.3"
   resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9"
@@ -2440,17 +2355,17 @@ object.values@^1.1.6:
     define-properties "^1.1.4"
     es-abstract "^1.20.4"
 
+obliterator@^2.0.1:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-2.0.4.tgz#fa650e019b2d075d745e44f1effeb13a2adbe816"
+  integrity sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==
+
 on-exit-leak-free@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz#5c703c968f7e7f851885f6459bf8a8a57edc9cc4"
   integrity sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==
 
-on-net-listen@^1.1.1:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/on-net-listen/-/on-net-listen-1.1.2.tgz#671e55a81c910fa7e5b1e4d506545e9ea0f2e11c"
-  integrity sha512-y1HRYy8s/RlcBvDUwKXSmkODMdx4KSuIvloCnQYJ2LdBBC1asY4HtfhXwe3UWknLakATZDnbzht2Ijw3M1EqFg==
-
-once@^1.3.0, once@^1.3.1, once@^1.4.0:
+once@^1.3.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
   integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
@@ -2471,7 +2386,7 @@ onetime@^6.0.0:
   dependencies:
     mimic-fn "^4.0.0"
 
-openapi-types@^12.0.0:
+openapi-types@^12.0.0, openapi-types@^12.0.2:
   version "12.1.0"
   resolved "https://registry.yarnpkg.com/openapi-types/-/openapi-types-12.1.0.tgz#bd01acc937b73c9f6db2ac2031bf0231e21ebff0"
   integrity sha512-XpeCy01X6L5EpP+6Hc3jWN7rMZJ+/k1lwki/kTmWzbVhdPie3jd5O2ZtedEx8Yp58icJ0osVldLMrTB/zslQXA==
@@ -2493,13 +2408,27 @@ os-tmpdir@~1.0.2:
   resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
   integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==
 
-p-limit@^3.0.2:
+p-limit@^2.0.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+  integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+  dependencies:
+    p-try "^2.0.0"
+
+p-limit@^3.0.2, p-limit@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
   integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
   dependencies:
     yocto-queue "^0.1.0"
 
+p-locate@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
+  integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
+  dependencies:
+    p-limit "^2.0.0"
+
 p-locate@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
@@ -2514,10 +2443,10 @@ p-map@^4.0.0:
   dependencies:
     aggregate-error "^3.0.0"
 
-pako@^1.0.3:
-  version "1.0.11"
-  resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
-  integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
+p-try@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+  integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
 
 parent-module@^1.0.0:
   version "1.0.1"
@@ -2526,6 +2455,11 @@ parent-module@^1.0.0:
   dependencies:
     callsites "^3.0.0"
 
+path-exists@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+  integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==
+
 path-exists@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
@@ -2573,7 +2507,7 @@ pidtree@^0.6.0:
   resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.6.0.tgz#90ad7b6d42d5841e69e0a2419ef38f8883aa057c"
   integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==
 
-pino-abstract-transport@^1.0.0, pino-abstract-transport@v1.0.0:
+pino-abstract-transport@v1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz#cc0d6955fffcadb91b7b49ef220a6cc111d48bb3"
   integrity sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==
@@ -2581,42 +2515,12 @@ pino-abstract-transport@^1.0.0, pino-abstract-transport@v1.0.0:
     readable-stream "^4.0.0"
     split2 "^4.0.0"
 
-pino-http@^8.2.1:
-  version "8.3.3"
-  resolved "https://registry.yarnpkg.com/pino-http/-/pino-http-8.3.3.tgz#2b140e734bfc6babe0df272a43bb8f36f2b525c0"
-  integrity sha512-p4umsNIXXVu95HD2C8wie/vXH7db5iGRpc+yj1/ZQ3sRtTQLXNjoS6Be5+eI+rQbqCRxen/7k/KSN+qiZubGDw==
-  dependencies:
-    get-caller-file "^2.0.5"
-    pino "^8.0.0"
-    pino-std-serializers "^6.0.0"
-    process-warning "^2.0.0"
-
-pino-pretty@^9.1.1:
-  version "9.4.0"
-  resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-9.4.0.tgz#fc4026e83c87272cbdfb7afed121770e6000940c"
-  integrity sha512-NIudkNLxnl7MGj1XkvsqVyRgo6meFP82ECXF2PlOI+9ghmbGuBUUqKJ7IZPIxpJw4vhhSva0IuiDSAuGh6TV9g==
-  dependencies:
-    colorette "^2.0.7"
-    dateformat "^4.6.3"
-    fast-copy "^3.0.0"
-    fast-safe-stringify "^2.1.1"
-    help-me "^4.0.1"
-    joycon "^3.1.1"
-    minimist "^1.2.6"
-    on-exit-leak-free "^2.1.0"
-    pino-abstract-transport "^1.0.0"
-    pump "^3.0.0"
-    readable-stream "^4.0.0"
-    secure-json-parse "^2.4.0"
-    sonic-boom "^3.0.0"
-    strip-json-comments "^3.1.1"
-
 pino-std-serializers@^6.0.0:
   version "6.1.0"
   resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.1.0.tgz#307490fd426eefc95e06067e85d8558603e8e844"
   integrity sha512-KO0m2f1HkrPe9S0ldjx7za9BJjeHqBku5Ch8JyxETxT8dEFGz1PwgrHaOQupVYitpzbFSYm7nnljxD8dik2c+g==
 
-pino@^8.0.0, pino@^8.5.0, pino@^8.6.1:
+pino@^8.5.0:
   version "8.11.0"
   resolved "https://registry.yarnpkg.com/pino/-/pino-8.11.0.tgz#2a91f454106b13e708a66c74ebc1c2ab7ab38498"
   integrity sha512-Z2eKSvlrl2rH8p5eveNUnTdd4AjJk8tAsLkHYZQKGHP4WTh2Gi1cOSOs3eWPqaj+niS3gj4UkoreoaWgF3ZWYg==
@@ -2633,16 +2537,18 @@ pino@^8.0.0, pino@^8.5.0, pino@^8.6.1:
     sonic-boom "^3.1.0"
     thread-stream "^2.0.0"
 
+pkg-up@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5"
+  integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==
+  dependencies:
+    find-up "^3.0.0"
+
 prelude-ls@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
   integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
 
-pretty-bytes@^5.4.1:
-  version "5.6.0"
-  resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
-  integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
-
 process-warning@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-2.1.0.tgz#1e60e3bfe8183033bbc1e702c2da74f099422d1a"
@@ -2653,11 +2559,6 @@ process@^0.11.10:
   resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
   integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
 
-progress@^2.0.3:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
-  integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
-
 prom-client@^14.1.0:
   version "14.2.0"
   resolved "https://registry.yarnpkg.com/prom-client/-/prom-client-14.2.0.tgz#ca94504e64156f6506574c25fb1c34df7812cf11"
@@ -2673,31 +2574,11 @@ proxy-addr@^2.0.7:
     forwarded "0.2.0"
     ipaddr.js "1.9.1"
 
-pstree.remy@^1.1.8:
-  version "1.1.8"
-  resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a"
-  integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==
-
-pump@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
-  integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
-  dependencies:
-    end-of-stream "^1.1.0"
-    once "^1.3.1"
-
 punycode@^2.1.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
   integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
 
-qs@^6.11.0:
-  version "6.11.1"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.1.tgz#6c29dff97f0c0060765911ba65cbc9764186109f"
-  integrity sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==
-  dependencies:
-    side-channel "^1.0.4"
-
 queue-microtask@^1.2.2:
   version "1.2.3"
   resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
@@ -2723,15 +2604,6 @@ randombytes@^2.1.0:
   dependencies:
     safe-buffer "^5.1.0"
 
-readable-stream@^3.6.0:
-  version "3.6.2"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
-  integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
-  dependencies:
-    inherits "^2.0.3"
-    string_decoder "^1.1.1"
-    util-deprecate "^1.0.1"
-
 readable-stream@^4.0.0:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.3.0.tgz#0914d0c72db03b316c9733bb3461d64a3cc50cba"
@@ -2785,11 +2657,6 @@ regexpp@^3.0.0:
   resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
   integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
 
-reinterval@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/reinterval/-/reinterval-1.1.0.tgz#3361ecfa3ca6c18283380dd0bb9546f390f5ece7"
-  integrity sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==
-
 require-directory@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@@ -2837,11 +2704,6 @@ ret@~0.2.0:
   resolved "https://registry.yarnpkg.com/ret/-/ret-0.2.2.tgz#b6861782a1f4762dce43402a71eb7a283f44573c"
   integrity sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==
 
-retimer@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/retimer/-/retimer-3.0.0.tgz#98b751b1feaf1af13eb0228f8ea68b8f9da530df"
-  integrity sha512-WKE0j11Pa0ZJI5YIk0nflGI7SQsfl2ljihVy7ogh7DeQSeYAUi0ubZ/yEueGtDfUPk6GH5LRw1hBdLq4IwUBWA==
-
 reusify@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
@@ -2885,7 +2747,7 @@ rxjs@^7.8.0:
   dependencies:
     tslib "^2.1.0"
 
-safe-buffer@^5.1.0, safe-buffer@~5.2.0:
+safe-buffer@5.2.1, safe-buffer@^5.1.0:
   version "5.2.1"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
   integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
@@ -2911,7 +2773,7 @@ safe-stable-stringify@^2.3.1, safe-stable-stringify@^2.4.1:
   resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886"
   integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==
 
-secure-json-parse@^2.4.0, secure-json-parse@^2.5.0:
+secure-json-parse@^2.5.0:
   version "2.7.0"
   resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz#5a5f9cd6ae47df23dba3151edd06855d47e09862"
   integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==
@@ -2921,11 +2783,6 @@ semver-compare@^1.0.0:
   resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
   integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==
 
-semver@^5.7.1:
-  version "5.7.1"
-  resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
-  integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
-
 semver@^6.3.0:
   version "6.3.0"
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
@@ -2938,11 +2795,6 @@ semver@^7.0.0, semver@^7.3.2, semver@^7.3.7, semver@^7.3.8:
   dependencies:
     lru-cache "^6.0.0"
 
-semver@~7.0.0:
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
-  integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
-
 serialize-error@^8.1.0:
   version "8.1.0"
   resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-8.1.0.tgz#3a069970c712f78634942ddd50fbbc0eaebe2f67"
@@ -2993,13 +2845,6 @@ signal-exit@^3.0.2, signal-exit@^3.0.7:
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
   integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
 
-simple-update-notifier@^1.0.7:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz#67694c121de354af592b347cdba798463ed49c82"
-  integrity sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==
-  dependencies:
-    semver "~7.0.0"
-
 sinon@^15.0.3:
   version "15.0.3"
   resolved "https://registry.yarnpkg.com/sinon/-/sinon-15.0.3.tgz#38005fcd80827177b6aa0245f82401d9ec88994b"
@@ -3038,7 +2883,7 @@ slice-ansi@^5.0.0:
     ansi-styles "^6.0.0"
     is-fullwidth-code-point "^4.0.0"
 
-sonic-boom@^3.0.0, sonic-boom@^3.1.0:
+sonic-boom@^3.1.0:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.3.0.tgz#cffab6dafee3b2bcb88d08d589394198bee1838c"
   integrity sha512-LYxp34KlZ1a2Jb8ZQgFCK3niIHzibdwtwNUWKg0qQRzsDoJ3Gfgkf8KdBTFU3SkejDEIlWwnSnpVdOZIhFMl/g==
@@ -3046,9 +2891,9 @@ sonic-boom@^3.0.0, sonic-boom@^3.1.0:
     atomic-sleep "^1.0.0"
 
 split2@^4.0.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/split2/-/split2-4.1.0.tgz#101907a24370f85bb782f08adaabe4e281ecf809"
-  integrity sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4"
+  integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==
 
 sprintf-js@^1.1.1:
   version "1.1.2"
@@ -3120,13 +2965,6 @@ string.prototype.trimstart@^1.0.6:
     define-properties "^1.1.4"
     es-abstract "^1.20.4"
 
-string_decoder@^1.1.1:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
-  integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
-  dependencies:
-    safe-buffer "~5.2.0"
-
 stringify-object-es5@^2.5.0:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/stringify-object-es5/-/stringify-object-es5-2.5.0.tgz#057c3c9a90a127339bb9d1704a290bb7bd0a1ec5"
@@ -3164,37 +3002,6 @@ strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
   integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
 
-subarg@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2"
-  integrity sha512-RIrIdRY0X1xojthNcVtgT9sjpOGagEUKpZdgBUi054OEPFo282yg+zE+t1Rj3+RqKq2xStL7uUHhY+AjbC4BXg==
-  dependencies:
-    minimist "^1.1.0"
-
-superagent@^8.0.5, superagent@^8.0.9:
-  version "8.0.9"
-  resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.0.9.tgz#2c6fda6fadb40516515f93e9098c0eb1602e0535"
-  integrity sha512-4C7Bh5pyHTvU33KpZgwrNKh/VQnvgtCSqPRfJAUdmrtSYePVzVg4E4OzsrbkhJj9O7SO6Bnv75K/F8XVZT8YHA==
-  dependencies:
-    component-emitter "^1.3.0"
-    cookiejar "^2.1.4"
-    debug "^4.3.4"
-    fast-safe-stringify "^2.1.1"
-    form-data "^4.0.0"
-    formidable "^2.1.2"
-    methods "^1.1.2"
-    mime "2.6.0"
-    qs "^6.11.0"
-    semver "^7.3.8"
-
-supertest@^6.3.3:
-  version "6.3.3"
-  resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.3.3.tgz#42f4da199fee656106fd422c094cf6c9578141db"
-  integrity sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA==
-  dependencies:
-    methods "^1.1.2"
-    superagent "^8.0.5"
-
 supports-color@8.1.1:
   version "8.1.1"
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
@@ -3202,13 +3009,6 @@ supports-color@8.1.1:
   dependencies:
     has-flag "^4.0.0"
 
-supports-color@^5.5.0:
-  version "5.5.0"
-  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
-  integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
-  dependencies:
-    has-flag "^3.0.0"
-
 supports-color@^7.1.0, supports-color@^7.2.0:
   version "7.2.0"
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
@@ -3228,7 +3028,7 @@ tdigest@^0.1.1:
   dependencies:
     bintrees "1.0.2"
 
-testdouble@^3.17.2:
+testdouble@^3.17.1:
   version "3.17.2"
   resolved "https://registry.yarnpkg.com/testdouble/-/testdouble-3.17.2.tgz#a7d624c2040453580b4a636b3f017bf183a8f487"
   integrity sha512-oRrk1DJISNoFr3aaczIqrrhkOUQ26BsXN3SopYT/U0GTvk9hlKPCEbd9R2uxkcufKZgEfo9D1JAB4CJrjHE9cw==
@@ -3260,15 +3060,10 @@ through@^2.3.8:
   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
   integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
 
-timestring@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/timestring/-/timestring-6.0.0.tgz#b0c7c331981ecf2066ce88bcfb8ee3ae32e7a0f6"
-  integrity sha512-wMctrWD2HZZLuIlchlkE2dfXJh7J2KDI9Dwl+2abPYg0mswQHfOAyQW3jJg1pY5VfttSINZuKcXoB3FGypVklA==
-
 tiny-lru@^10.0.0:
-  version "10.2.2"
-  resolved "https://registry.yarnpkg.com/tiny-lru/-/tiny-lru-10.2.2.tgz#dba7e921a9f940e841a7f4e5aef1cb478ecdb07f"
-  integrity sha512-yx+e2W/6E0SBEuXG+3M1fibQyyMDbz3jd/8EPyodw+LQVcUXA6tfNGUzktEy1PgZlresc6PiC4HQiyeZM2lGzA==
+  version "10.3.0"
+  resolved "https://registry.yarnpkg.com/tiny-lru/-/tiny-lru-10.3.0.tgz#2ddc88bbe8d9a2c761df673ebef52a82182a64b3"
+  integrity sha512-vTKRT2AEO1sViFDWAIzZVpV8KURCaMtnHa4RZB3XqtYLbrTO/fLDXKPEX9kVWq9u+nZREkwakbcmzGgvJm8QKA==
 
 tmp@^0.0.33:
   version "0.0.33"
@@ -3289,13 +3084,6 @@ toidentifier@1.0.1:
   resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
   integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
 
-touch@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"
-  integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==
-  dependencies:
-    nopt "~1.0.10"
-
 tsconfig-paths@^3.14.1:
   version "3.14.2"
   resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088"
@@ -3338,6 +3126,14 @@ type-fest@^0.21.3:
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
   integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
 
+type-is@^1.6.18:
+  version "1.6.18"
+  resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
+  integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
+  dependencies:
+    media-typer "0.3.0"
+    mime-types "~2.1.24"
+
 typed-array-length@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb"
@@ -3357,11 +3153,6 @@ unbox-primitive@^1.0.2:
     has-symbols "^1.0.3"
     which-boxed-primitive "^1.0.2"
 
-undefsafe@^2.0.5:
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c"
-  integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==
-
 uri-js@^4.2.2:
   version "4.4.1"
   resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
@@ -3369,21 +3160,16 @@ uri-js@^4.2.2:
   dependencies:
     punycode "^2.1.0"
 
-util-deprecate@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
-  integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
-
-uuid-parse@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/uuid-parse/-/uuid-parse-1.1.0.tgz#7061c5a1384ae0e1f943c538094597e1b5f3a65b"
-  integrity sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A==
-
-uuid@^8.3.0, uuid@^8.3.2:
+uuid@^8.3.0:
   version "8.3.2"
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
   integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
 
+vary@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
+  integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
+
 which-boxed-primitive@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
@@ -3447,6 +3233,11 @@ wrappy@1:
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
   integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
 
+xml@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5"
+  integrity sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==
+
 y18n@^5.0.5:
   version "5.0.8"
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
@@ -3457,7 +3248,7 @@ yallist@^4.0.0:
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
   integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
 
-yaml@^2.1.1, yaml@^2.2.1:
+yaml@^2.1.1, yaml@^2.1.3, yaml@^2.2.1:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.2.1.tgz#3014bf0482dcd15147aa8e56109ce8632cd60ce4"
   integrity sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==
-- 
GitLab