diff --git a/.dockerignore b/.dockerignore index 8d918742e070032631aff29781c441e07510896f..9b89962f95047ce929a8415200a3ad66c9a0e4b3 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 a26510fc2416c632a88efa3d899d8e7d7b8e87fd..cce505ceeb37500f8a48f62c00127bce104d979e 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 0000000000000000000000000000000000000000..b7bcb494a16eaf2b4ea54af084f51f6cca360e3d --- /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 2ab02848a2151f0bfb9b29b063c087ac26beeae0..5bc5ccddbfb305fb3742220998b8849e7933ea41 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 af7db5190d2b0954ee7b30f094093ee1b09fa238..d12631ad44c6de3220690201abf598ec7e2b84b3 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 dfa5597c7bf170f10ab54af093e3e96c427bc3a8..0000000000000000000000000000000000000000 --- 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 232f3f9ea1b2bfd822dc2f61dd51f6dbda0fb453..3c82d5d34c5254739d46b2a0deac976bf0c36ed4 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 eb62a09e6db46b07606f7b0ba5a244e65f99a087..750b6314ae4d094bb62b462bc66399e886e0c1a0 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 e1b9d20ed3cc4fbaf5008f2e159a2723fbfba1bd..bb92cc8a937578d33216fc9d2b1888920237f51c 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 3acef19043dfa0e0b28caece6027b2280fb44d46..0000000000000000000000000000000000000000 --- 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 1de35decb08accdc45a646e5c58cee358901c6a1..ea8bd1d64e0c8686a8d24d9b0488dff7a1633943 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 0547a34a3ea490d5518f9a8fe359c9396d5db082..0000000000000000000000000000000000000000 --- 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 029da1e0ce31ce32589fcc4a34b0d590e4f4ebe2..67151e4f1ca1eb344c75ebceee33c2ccd3889bb7 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 f7b109a38e41152e2db4d50e8401edc0a129af55..3c434416b27c3de27359bf0731611876252211ae 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 db2ab6ee99b5f6ec0db8e8cac87d5cecefb26a22..31143a1e872e20511ddaf67aba283feb9352f0de 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 1cf64bd0ceb38e7b4ddda38b405902ba046f878d..0523bcc37c6698e7ee8e5fda4ef9131271032c17 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 faca45bed45436a9f405bea36e77a3dac389a4f6..abc4c72f488d975d67d339b9bdf800473284b445 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 b9ca8a1374bef547873d8b6374e2efcfc8ed515d..e9a243796b65722a8f598b473dbb928f0cd51814 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 e27d9c51795315ef366bb6abcd03e92fd2f81740..1fedfbe1fbb0da5bee520ab67dc97bcd920b2c82 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 0baf75067eb07d7d5f0a2b9059dd73443f02ad94..0e20fbcb8281ca4c872a0330241a2d1f7ffa687e 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 cc51b841a1bcc795c2acf8ea089b27700708954b..7af4f677be9ea91aa6b48042fdbe2c320dd549ad 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 a3b36325d4a8577e87da43e7272860d85dff92c9..88fbe750486e81b2ffd2dec4aef85becc9f32327 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 0e015284575ba0955a65d32c98716769eddf6368..0983bae48a521f3a3542fcb3ceb520bf4d9094b6 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 c29bab744690324e3aaa1d6b7117f5432890c76d..04a3a341e409d043e74896a39d71835ae978d6af 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 236f7679150f1388d87a6013e34dfc94b02605d2..5b4e0a8f6ed2706015b74871ec4973da56c6c9e9 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 222529aeebb06cf91dbf7fefa2651f3645ddc549..7d958d83e4f0d0b20fc43f8797a04b859454fd18 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 422633af651031637c7d51364a694ab85a7b3800..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..1b27e377953de476de3e096dfaf1bdbbe2ed0575 --- /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 1c6a27b8090c61967f3cdca2a54c66366cb1fe78..0000000000000000000000000000000000000000 --- 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 0ee22f75886686385bc26235fafd31e4ef99b736..5d852c474a25901a46ce368cbc588682fb4151ad 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 d02e90fee188b0403da660dd707a2727d0bb658d..0000000000000000000000000000000000000000 --- 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 3b4b7172ec3a65110c593072a7dee401e6f8b336..5ef2eeaa41d691648979ed1aad1d4aba8f59fa5b 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 0000000000000000000000000000000000000000..066054bb56d55e71c68cad3dd81eee71c6294d8a --- /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 f1199ed0c190dec9b358c0796a13f00f8bff58d9..a7c5ba3ee6d6466afb2c38471b34b5961f5f1aa3 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 221e0e09cf18c278f32e77e4fb8d0f6ee7533847..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..dc3514580985e13ed4bdcb6a16d32b6aa233976b --- /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 0000000000000000000000000000000000000000..8244babc913896710df04e30a0ccd7af7310515e --- /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 0000000000000000000000000000000000000000..00c9f35324746919b590fbbcfc84977b51ab3391 --- /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 0000000000000000000000000000000000000000..87f47700a67fc0085f36ac24e1cf23aadb0cfbe6 --- /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 81917880dde1b1fc54bdc211a0d7102e24ea964a..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..7d13ae598ee4fb45132212d7fd0a57dd81fce5ab --- /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 0000000000000000000000000000000000000000..f31f3204e80f9a87c9a8fd555153952d841e1de2 --- /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 0000000000000000000000000000000000000000..81f31a2bb6e2085c0395a90beeadbe030fc92bf3 --- /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 0000000000000000000000000000000000000000..b7dcceb09d0a7c0a497aa7105f1863028f7f9c66 --- /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 21279936005d94c12f6eca8881db620fc6281c3e..6c78ae581a8f4fdfdff385e21bd344742f18eaba 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 0000000000000000000000000000000000000000..41320ea8679059c372e9db9fafafe49fa56c10f0 --- /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 5676ad2674edaf0fdfeb5340e5480676a0c6b2f4..21f5514e35444a35abd34a312980b49d0fe7dbb6 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 17adfb27e691eeef0e7cab4ac82fa17747ec6c50..bc81c21e49f5a97d7985cb3a60688aaf92af368e 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 d057a2b19b6a82921c5b17cf2e4b5e02eb4fbf9a..e2921c8f6a5bac58926560c36444aa577a2d06cd 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 aa5ba3e8fddde1efe79cc45b4ed1c36ca47898f1..f0f2a8478c742c8764b29c287b42c125d5f4eebd 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 495121399ab11a19938b6652de2a4a93f8565f74..bbd13c46237f70d3ed5ca81376e6a58227be9cee 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 70c736063f1a95117fbd9f4099e12dd79a6694df..f9b14804d7741a4f9cbfbc9153fbf50a31863151 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 54f42248db37c9354697c173b72f103c31cfaa68..0000000000000000000000000000000000000000 --- 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 25a6b9e4f4d75309125f84a79482fae150d0741d..3794fc35d7fdf5d8f040baaf60f63b24672b60a6 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 857b1dce291374276634d586bf3facaa871bd314..f5afffb8a7e53bbf7f5181012e43404d5fe57e49 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==