From 683bd387e933d62c7c7982642d88868fccbbbdb1 Mon Sep 17 00:00:00 2001 From: Bleron Qorri Date: Wed, 12 Mar 2025 16:33:04 +0100 Subject: [PATCH 01/22] Added initial solution v1 - without ELK stack --- backend/.dockerignore | 24 + backend/.gitignore | 114 + backend/Dockerfile | 10 + backend/docker-compose.yml | 64 + backend/jsconfig.json | 8 + backend/package-lock.json | 3600 +++++++++++++++++ backend/package.json | 31 + backend/src/config/index.js | 16 + backend/src/data/models/Log.js | 17 + backend/src/data/models/Source.js | 53 + backend/src/data/models/db.js | 22 + backend/src/server/app.js | 66 + .../server/background-processing/bullmq.js | 29 + .../callback-api-handler.js | 128 + .../log-fetching-scheduler.js | 59 + .../server/background-processing/logger.js | 16 + .../background-processing/redis-connection.js | 10 + .../server/controllers/source.controller.js | 61 + .../server/middlewares/apiKey.preHandler.js | 22 + backend/src/server/routes/source.routes.js | 23 + backend/src/server/schemas/source.schemas.js | 170 + backend/src/server/seed.js | 31 + backend/src/server/server.js | 13 + backend/src/server/services/index.js | 0 backend/src/server/swagger.js | 31 + backend/src/server/validations/index.js | 0 26 files changed, 4618 insertions(+) create mode 100644 backend/.dockerignore create mode 100644 backend/.gitignore create mode 100644 backend/Dockerfile create mode 100644 backend/docker-compose.yml create mode 100644 backend/jsconfig.json create mode 100644 backend/package-lock.json create mode 100644 backend/package.json create mode 100644 backend/src/config/index.js create mode 100644 backend/src/data/models/Log.js create mode 100644 backend/src/data/models/Source.js create mode 100644 backend/src/data/models/db.js create mode 100644 backend/src/server/app.js create mode 100644 backend/src/server/background-processing/bullmq.js create mode 100644 backend/src/server/background-processing/callback-api-handler.js create mode 100644 backend/src/server/background-processing/log-fetching-scheduler.js create mode 100644 backend/src/server/background-processing/logger.js create mode 100644 backend/src/server/background-processing/redis-connection.js create mode 100644 backend/src/server/controllers/source.controller.js create mode 100644 backend/src/server/middlewares/apiKey.preHandler.js create mode 100644 backend/src/server/routes/source.routes.js create mode 100644 backend/src/server/schemas/source.schemas.js create mode 100644 backend/src/server/seed.js create mode 100644 backend/src/server/server.js create mode 100644 backend/src/server/services/index.js create mode 100644 backend/src/server/swagger.js create mode 100644 backend/src/server/validations/index.js diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 00000000..809ba837 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,24 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 00000000..73b01534 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,114 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables folder and file +.env + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Ignore databases +*.sqlite +*.db + +# Ignore editor folder +.vscode +.idea + +# Imagine Stuff +.imagine diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 00000000..b7ad0e6a --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,10 @@ +FROM node:lts-alpine +ENV NODE_ENV=DEVELOPMENT +WORKDIR /usr/src/app +COPY ["package.json", "package-lock.json*", "npm-shrinkwrap.json*", "./"] +RUN npm install --silent && mv node_modules ../ +COPY . . +EXPOSE 3000 +RUN chown -R node /usr/src/app +USER node +CMD ["npm", "start"] diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 00000000..8348aacf --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,64 @@ +version: '3.4' + +services: + backendchallenge: + image: backendchallenge + build: + context: . + dockerfile: ./Dockerfile + environment: + - API_KEY=${API_KEY} + - MONGO_URI=mongodb://mongo:27017/sourcedb + - ENCRYPTION_KEY=${ENCRYPTION_KEY} + - REDIS_HOST=redis-server + - REDIS_PORT=6379 + - NODE_DEBUG=bull + - NODE_ENV=${NODE_ENV} + - CALLBACK_API_HOOK=http://callbackapi:8080/Hooks/SendLog + ports: + - 3000:3000 + - 9229:9229 + command: ["npm", "start"] + depends_on: + - mongo + - redis-server + networks: + - backend-challenge-network + + mongo: + image: mongo:latest + container_name: mongo + ports: + - "27017:27017" + volumes: + - mongo_data:/data/db + networks: + - backend-challenge-network + + redis-server: + image: redis:latest + container_name: redis-server + ports: + - "6379:6379" + networks: + - backend-challenge-network + volumes: + - redis_data:/data + + callbackapi: + image: bleronqorri/callbackapi:latest + container_name: callbackapi + environment: + - ASPNETCORE_ENVIRONMENT=Development + ports: + - "8080:8080" + networks: + - backend-challenge-network + +volumes: + mongo_data: + redis_data: + +networks: + backend-challenge-network: + driver: bridge diff --git a/backend/jsconfig.json b/backend/jsconfig.json new file mode 100644 index 00000000..a6c04a4b --- /dev/null +++ b/backend/jsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "baseUrl": "./src", + "paths": { + "server/*": ["server/*"] + } + } +} diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 00000000..29db4b80 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,3600 @@ +{ + "name": "backend-challenge", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "backend-challenge", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@fastify/autoload": "^6.2.0", + "@fastify/swagger": "^9.4.2", + "@fastify/swagger-ui": "^5.2.2", + "axios": "^1.8.2", + "axios-retry": "^4.5.0", + "bottleneck": "^2.19.5", + "bullmq": "^5.41.8", + "dotenv": "^16.4.7", + "fastify": "^5.2.1", + "ioredis": "^5.6.0", + "mongoose": "^8.12.1", + "pino": "^9.6.0", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1" + }, + "devDependencies": { + "nodemon": "^3.1.9", + "pino-pretty": "^13.0.0" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@fastify/accept-negotiator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.0.1.tgz", + "integrity": "sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.2.tgz", + "integrity": "sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/autoload": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@fastify/autoload/-/autoload-6.2.0.tgz", + "integrity": "sha512-NcvIcAqa6z/rpENcLHEMtLMjrXgWge8MyzTaPbI+svzEzTd9vaDdC47N6WugZBrqaOTSKX4PmEAsYNOn8jPzkg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.0.0.tgz", + "integrity": "sha512-OO/SA8As24JtT1usTUTKgGH7uLvhfwZPwlptRi2Dp5P4KKmJI3gvsZ8MIHnNwDs4sLf/aai5LzTyl66xr7qMxA==", + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.2.tgz", + "integrity": "sha512-YdR7gqlLg1xZAQa+SX4sMNzQHY5pC54fu9oC5aYSUqBhyn6fkLkrdtKlpVdCNPlwuUuXA1PjFTEmvMF6ZVXVGw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^6.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.0.tgz", + "integrity": "sha512-kJExsp4JCms7ipzg7SJ3y8DwmePaELHxKYtg+tZow+k0znUTf3cb+npgyqm8+ATZOdmfgfydIebPDWM172wfyA==", + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.0.0.tgz", + "integrity": "sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA==", + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@fastify/proxy-addr/node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@fastify/send": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@fastify/send/-/send-3.3.1.tgz", + "integrity": "sha512-6pofeVwaHN+E/MAofCwDqkWUliE3i++jlD0VH/LOfU8TJlCkMUSgKvA9bawDdVXxjve7XrdYMyDmkiYaoGWEtA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "escape-html": "~1.0.3", + "fast-decode-uri-component": "^1.0.1", + "http-errors": "^2.0.0", + "mime": "^3" + } + }, + "node_modules/@fastify/send/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@fastify/static": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-8.1.1.tgz", + "integrity": "sha512-TW9eyVHJLytZNpBlSIqd0bl1giJkEaRaPZG+5AT3L/OBKq9U8D7g/OYmc2NPQZnzPURGhMt3IAWuyVkvd2nOkQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/accept-negotiator": "^2.0.0", + "@fastify/send": "^3.2.0", + "content-disposition": "^0.5.4", + "fastify-plugin": "^5.0.0", + "fastq": "^1.17.1", + "glob": "^11.0.0" + } + }, + "node_modules/@fastify/static/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@fastify/static/node_modules/glob": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", + "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@fastify/static/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@fastify/swagger": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@fastify/swagger/-/swagger-9.4.2.tgz", + "integrity": "sha512-WjSUu6QnmysLx1GeX7+oQAQUG/vBK5L8Qzcsht2SEpZiykpHURefMZpf+u3XbwSuH7TjeWOPgGIJIsEgj8AvxQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "json-schema-resolver": "^3.0.0", + "openapi-types": "^12.1.3", + "rfdc": "^1.3.1", + "yaml": "^2.4.2" + } + }, + "node_modules/@fastify/swagger-ui": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@fastify/swagger-ui/-/swagger-ui-5.2.2.tgz", + "integrity": "sha512-jf8xe+D8Xjc8TqrZhtlJImOWihd8iYFu8dhM01mGg+F04CKUM0zGB9aADE9nxzRUszyWp3wn+uWk89nbAoBMCw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/static": "^8.0.0", + "fastify-plugin": "^5.0.0", + "openapi-types": "^12.1.3", + "rfdc": "^1.3.1", + "yaml": "^2.4.1" + } + }, + "node_modules/@fastify/swagger-ui/node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@fastify/swagger/node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.0.tgz", + "integrity": "sha512-+ywrb0AqkfaYuhHs6LxKWgqbh3I72EpEgESCw37o+9qPx9WTCkgDm2B+eMrwehGtHBWHFU4GXvnSCNiFhhausg==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT", + "peer": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.1.0.tgz", + "integrity": "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==", + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/axios": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz", + "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios-retry": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", + "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", + "license": "Apache-2.0", + "dependencies": { + "is-retry-allowed": "^2.2.0" + }, + "peerDependencies": { + "axios": "0.x || 1.x" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bson": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.3.tgz", + "integrity": "sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/bullmq": { + "version": "5.41.8", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.8.tgz", + "integrity": "sha512-GUwvnYhxvpPLx8O3Npa9b/txZCfUTcm2F6FNxwCdARk01ZRutoGEOhnAZLHjKgUyfVAN2AB9kbdb4sHjCdBYMQ==", + "license": "MIT", + "dependencies": { + "cron-parser": "^4.9.0", + "ioredis": "^5.4.1", + "msgpackr": "^1.11.2", + "node-abort-controller": "^3.1.1", + "semver": "^7.5.4", + "tslib": "^2.0.0", + "uuid": "^9.0.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT", + "peer": true + }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "license": "MIT", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT", + "peer": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-copy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.0.1.tgz", + "integrity": "sha512-s7SJE83QKBZwg54dIbD5rCtzOBVD43V1ReWXXYqBgwCwHLYAAT0RQc/FmrQglXqWPpz6omtryJQOau5jI4Nrvg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0", + "json-schema-ref-resolver": "^2.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.2.1.tgz", + "integrity": "sha512-rslrNBF67eg8/Gyn7P2URV8/6pz8kSAscFL4EThZJ8JBMaXacVdVE4hmUcnPNKERl5o/xTiBSLfdowBRhVF1WA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.0", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^6.0.0", + "find-my-way": "^9.0.0", + "light-my-request": "^6.0.0", + "pino": "^9.0.0", + "process-warning": "^4.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^3.0.1", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastify-plugin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.0.1.tgz", + "integrity": "sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-my-way": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.2.0.tgz", + "integrity": "sha512-d3uCir8Hmg7W1Ywp8nKf2lJJYU9Nwinvo+1D39Dn09nz65UKXIxUh7j7K8zeWhxqe1WrkS7FJyON/Q/3lPoc6w==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ioredis": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.0.tgz", + "integrity": "sha512-tBZlIIWbndeWBWCXWZiqtOF/yxf6yZX3tAlTJ7nfo5jhd6dctNxF7QnYlZLZ1a0o0pDoen7CgZqO+zjNaFbJAg==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ioredis/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ioredis/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-retry-allowed": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", + "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", + "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-2.0.1.tgz", + "integrity": "sha512-HG0SIB9X4J8bwbxCbnd5FfPEbcXAJYTi1pBJeP/QPON+w8ovSME8iRG+ElHNxZNX2Qh6eYn1GdzJFS4cDFfx0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-resolver/-/json-schema-resolver-3.0.0.tgz", + "integrity": "sha512-HqMnbz0tz2DaEJ3ntsqtx3ezzZyDE7G56A/pPY/NGmrPu76UzsWquOpHFRAf5beTNXoH2LU5cQePVvRli1nchA==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fast-uri": "^3.0.5", + "rfdc": "^1.1.4" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/Eomm/json-schema-resolver?sponsor=1" + } + }, + "node_modules/json-schema-resolver/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/json-schema-resolver/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/luxon": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "peer": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mongodb": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.14.2.tgz", + "integrity": "sha512-kMEHNo0F3P6QKDq17zcDuPeaywK/YaJVCEQRzPF3TOM/Bl9MFg64YE5Tu7ifj37qZJMhwU1tl2Ioivws5gRG5Q==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.9", + "bson": "^6.10.3", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.12.1.tgz", + "integrity": "sha512-UW22y8QFVYmrb36hm8cGncfn4ARc/XsYWQwRTaj0gxtQk1rDuhzDO1eBantS+hTTatfAIS96LlRCJrcNHvW5+Q==", + "license": "MIT", + "dependencies": { + "bson": "^6.10.3", + "kareem": "2.6.3", + "mongodb": "~6.14.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/mquery/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mquery/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT", + "peer": true + }, + "node_modules/msgpackr": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.2.tgz", + "integrity": "sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "license": "MIT" + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/nodemon": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", + "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "peer": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT" + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT", + "peer": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.6.0.tgz", + "integrity": "sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^4.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.0.0.tgz", + "integrity": "sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.2", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "peer": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-4.0.1.tgz", + "integrity": "sha512-goqsB+bSlOmVX+CiFX2PFc1OV88j5jvBqIM+DgqrucHnUguAUNtiNOs+aTadq2NqsLQ+TQ3UEVG3gtSFcdlkCg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT", + "peer": true + }, + "node_modules/secure-json-parse": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-3.0.2.tgz", + "integrity": "sha512-H6nS2o8bWfpFEV6U38sOSjS7bTbdgbCGU9wEM6W14P5H0QOsz94KCusifV44GpHDTu2nqZbuDNhTzu+mjDSw1w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "peer": true + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "peer": true, + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/swagger-jsdoc": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "license": "MIT", + "dependencies": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.20.1.tgz", + "integrity": "sha512-qBPCis2w8nP4US7SvUxdJD3OwKcqiWeZmjN2VWhq2v+ESZEXOP/7n4DeiOiiZcGYTKMHAHUUrroHaTsjUWTEGw==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "peer": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.1.tgz", + "integrity": "sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ==", + "license": "MIT", + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 00000000..d1ecdb08 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,31 @@ +{ + "name": "backend-challenge", + "version": "1.0.0", + "description": "hi", + "scripts": { + "start": "npx nodemon src/server/server.js" + }, + "author": "", + "license": "ISC", + "type": "module", + "dependencies": { + "@fastify/autoload": "^6.2.0", + "@fastify/swagger": "^9.4.2", + "@fastify/swagger-ui": "^5.2.2", + "axios": "^1.8.2", + "axios-retry": "^4.5.0", + "bottleneck": "^2.19.5", + "bullmq": "^5.41.8", + "dotenv": "^16.4.7", + "fastify": "^5.2.1", + "ioredis": "^5.6.0", + "mongoose": "^8.12.1", + "pino": "^9.6.0", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1" + }, + "devDependencies": { + "nodemon": "^3.1.9", + "pino-pretty": "^13.0.0" + } +} diff --git a/backend/src/config/index.js b/backend/src/config/index.js new file mode 100644 index 00000000..6826643a --- /dev/null +++ b/backend/src/config/index.js @@ -0,0 +1,16 @@ +import dotenv from 'dotenv'; + +// loads environment variables from .env into process.env. Makes our app configurable. +dotenv.config(); + +// we are running on development +const env = process.env.NODE_ENV || 'development'; + +module.exports = { + dialect: process.env.DB_DIALECT, + storage: process.env.DB_STORAGE, + define: { + underscore: true, + }, + logging: false, +}; \ No newline at end of file diff --git a/backend/src/data/models/Log.js b/backend/src/data/models/Log.js new file mode 100644 index 00000000..2d178660 --- /dev/null +++ b/backend/src/data/models/Log.js @@ -0,0 +1,17 @@ +import mongoose from 'mongoose'; + +const LogSchema = new mongoose.Schema({ + id: { type: String, required: true, unique: true }, + payload: {type: Object, required: true }, + retryCount: {type: Number, required: false, default: 0}, + message: {type: String, required: false, default: ''}, + status: { + type: String, + enum: ['successful', 'failed', 'pending'], // Only these values are allowed + required: true + } +}, { timestamps: true }); + +const Log = mongoose.model('Log', LogSchema); + +export default Log; \ No newline at end of file diff --git a/backend/src/data/models/Source.js b/backend/src/data/models/Source.js new file mode 100644 index 00000000..e1402706 --- /dev/null +++ b/backend/src/data/models/Source.js @@ -0,0 +1,53 @@ +import mongoose from 'mongoose'; +import crypto from 'crypto'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const ENCRYPTION_KEY = Buffer.from(process.env.ENCRYPTION_KEY, 'hex'); +const IV_LENGTH = 16; + +function encrypt(text) { + const iv = crypto.randomBytes(IV_LENGTH); + const cipher = crypto.createCipheriv('aes-256-cbc', ENCRYPTION_KEY, iv); + let encrypted = cipher.update(text, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + return iv.toString('hex') + ':' + encrypted; + } + + function decrypt(text) { + const [ivHex, encryptedText] = text.split(':'); + const iv = Buffer.from(ivHex, 'hex'); + const decipher = crypto.createDecipheriv('aes-256-cbc', ENCRYPTION_KEY, iv); + let decrypted = decipher.update(encryptedText, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + return decrypted; + } + +const sourceSchema = new mongoose.Schema({ + id: { type: String, required: true, unique: true }, + sourceType: {type: String, required: true}, + callbackUrl: {type: String, required: true}, + logFetchInterval: {type: Number, required: true, min: 0, + validate: { + validator: Number.isInteger, + message: 'logFetchInterval must be an integer', + }, + }, + credentials: { type: String, required: true }, +}, { timestamps: true }); + +sourceSchema.pre('save', function (next) { + if (this.isModified('credentials')) { + this.credentials = encrypt(this.credentials); + } + next(); + }); + +sourceSchema.methods.getDecryptedData = function () { + return decrypt(this.credentials); + }; + +const Source = mongoose.model('Source', sourceSchema); + +export default Source; \ No newline at end of file diff --git a/backend/src/data/models/db.js b/backend/src/data/models/db.js new file mode 100644 index 00000000..8fff56c3 --- /dev/null +++ b/backend/src/data/models/db.js @@ -0,0 +1,22 @@ +import mongoose from 'mongoose'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const uri = process.env.MONGO_URI + +async function connectDB() { + try { + await mongoose.connect(uri, { + serverSelectionTimeoutMS: 5000, // ⏳ Wait max 5 sec for MongoDB + }); + + console.log('Connected to MongoDB using Mongoose'); + } catch (error) { + console.error('MongoDB Connection Error:', error); + process.exit(1); + } +} + + +export default connectDB; diff --git a/backend/src/server/app.js b/backend/src/server/app.js new file mode 100644 index 00000000..58031925 --- /dev/null +++ b/backend/src/server/app.js @@ -0,0 +1,66 @@ +import Fastify from 'fastify'; +import { setupSwagger } from './swagger.js'; +import AutoLoad from '@fastify/autoload'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; +import apiKeyPreHandler from './middlewares/apiKey.preHandler.js'; +import connectDB from '../data/models/db.js'; +import './background-processing/bullmq.js' +import seedDb from './seed.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const envToLogger = { + DEVELOPMENT: { + transport: { + targets: [ + { + level: 'info', + target: 'pino-pretty', + options: {} + } + ], + }, + }, + production: true, + test: false, +} + +const app = Fastify({ logger: envToLogger[process.env.NODE_ENV] ?? true }); + +await connectDB(); +await seedDb(); + +await setupSwagger(app); + +await app.after(); + +app.addHook('preHandler', apiKeyPreHandler); + +app.register(AutoLoad, { + dir: join(__dirname, 'routes'), +}); + +app.setErrorHandler((error, request, reply) => { + request.log.error(error); + + const status = error.statusCode || 500; + + const problemDetails = { + type: error.type || 'https://httpstatuses.com/' + status, + title: error.title || 'An error occurred', + status, + detail: error.message || 'Internal Server Error', + instance: request.url + }; + + reply + .status(status) + .header('Content-Type', 'application/problem+json') + .send(problemDetails); +}); + + +export default app; + diff --git a/backend/src/server/background-processing/bullmq.js b/backend/src/server/background-processing/bullmq.js new file mode 100644 index 00000000..c959ebc1 --- /dev/null +++ b/backend/src/server/background-processing/bullmq.js @@ -0,0 +1,29 @@ +import { Queue, Worker } from 'bullmq'; +import {scheduleLogFetchingJobs} from './log-fetching-scheduler.js' +import { redisConnection } from './redis-connection.js'; + + +export const logFetchingSchedulerQueue = new Queue('log-fetching-scheduler', { connection: redisConnection }); + +await logFetchingSchedulerQueue.upsertJobScheduler( + 'check-source-entries', + { + every: 3000, + }, + { + data: {}, + opts: { + removeOnComplete: true + }, + } + ); + + new Worker( + 'log-fetching-scheduler', + async job => await scheduleLogFetchingJobs(job), + { connection: redisConnection } + ); + + + +export const sourceJobsQueue = new Queue('source-jobs', { connection: redisConnection }); \ No newline at end of file diff --git a/backend/src/server/background-processing/callback-api-handler.js b/backend/src/server/background-processing/callback-api-handler.js new file mode 100644 index 00000000..ada5034b --- /dev/null +++ b/backend/src/server/background-processing/callback-api-handler.js @@ -0,0 +1,128 @@ +import Source from "../../data/models/Source.js"; +import axios from "axios"; +import axiosRetry from "axios-retry"; +import logger from "./logger.js"; +import Log from "../../data/models/Log.js"; + +axiosRetry(axios, { + retries: 3, + retryCondition: (error) => { + return error.response && ( + error.response.status === 429 || + error.response.status === 503 || + error.response.status === 500 || + error.response.status === 400 + ); + }, + retryDelay: (retryCount, error) => { + if (error.response && error.response.status === 429) { + const retryAfter = error.response.headers['retry-after']; + if (retryAfter) { + logger.info(`Retrying after ${parseInt(retryAfter)} milliseconds`) + return parseInt(retryAfter); + } + } + return Math.pow(2, retryCount) * 1000; + }, + onRetry: (retryCount, error, requestConfig) => { + var data = JSON.parse(requestConfig.data); + logger.warn(`Encountered error: ${error.message}. Retrying... Attempt ${retryCount} for request: ${data.id}`); + return; + }, + onMaxRetryTimesExceeded: async (error, retryCount) => { + logger.error(error, 'Max retries exceeded for request'); + var log = JSON.parse(error.config.data); + + await Log.updateOne( + { id: log.id }, + { $set: + { + id: log.id, + retryCount: retryCount, + message: error.message, + status:'failed' + } + }, + { upsert: true } + ); + + return; + } + +}); + +export const handleLogFetch = async (job) => { + const {id} = job.data; + + var source = await Source.findOne({id: id}); + + if(!source){ + logger.warn('no source found.') + job.remove(); + } + + logger.info(`processing source...id: ${id}`); + + // call google api + // let's pretend api returned successfully. logs are my deserialized response + const logs = generateLogs(); + + for (const log of logs) { + try { + const dbLog = Log.findOne({id:log.id}); + + if(dbLog && dbLog.status === 'successful') { + logger.info(`Log with id = ${dbLog.id} has already been processed before`); + continue; + } + + // add additional cases. For logs that have failed over three times, we may want a different handle. For now, let's just retry them. + + logger.info('sending request...' + log.id); + const response = await axios.post(source.callbackUrl, log); + logger.info('received response'); + if(response.status === 200){ + logger.info(`log with id ${log.id} was successfully processed`); + await Log.updateOne( + { id: log.id }, + { $set: + { + id: log.id, + payload: log, + status:'successful' + } + }, + { upsert: true } + ); + + } + } catch (error){ + logger.error(error, `log with id ${log.id} was not processed successfully`); + } + } + + logger.info('success!'); +} + + // to-do: remove once unblocked + const generateLogs = () => { + let logs = []; + + for (let i = 0; i < 5000; i++) { + const log = { + id: `log-id-${i + 1}`, + timestamp: new Date().toISOString(), // Current timestamp + actor: { + email: `user${Math.floor(Math.random() * 1000)}@example.com`, // Random email + ipAddress: `${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}` // Random IP address + }, + eventType: Math.random() > 0.5 ? 'LOGIN' : 'LOGOUT', // Random LOGIN or LOGOUT event + details: { + status: Math.random() > 0.5 ? 'SUCCESS' : 'FAILURE' // Random status: SUCCESS or FAILURE + } + }; + logs.push(log); + } + + return logs; + } \ No newline at end of file diff --git a/backend/src/server/background-processing/log-fetching-scheduler.js b/backend/src/server/background-processing/log-fetching-scheduler.js new file mode 100644 index 00000000..7d6e6ab9 --- /dev/null +++ b/backend/src/server/background-processing/log-fetching-scheduler.js @@ -0,0 +1,59 @@ +import Source from "../../data/models/Source.js" +import {sourceJobsQueue, logFetchingSchedulerQueue} from './bullmq.js' +import { Worker } from "bullmq" +import { redisConnection } from './redis-connection.js'; +import axios from "axios"; +import { handleLogFetch } from "./callback-api-handler.js"; +import logger from './logger.js'; + +export const scheduleLogFetchingJobs = async (job) => { + try{ + + const sources = await Source.find(); + + if(sources.length === 0){ + logger.info('no sources to schedule for now...'); + return; + } + + const waitingJobs = await sourceJobsQueue.getJobs(['waiting']); + const activeJobs = await sourceJobsQueue.getJobs(['active']); + const delayedJobs = await sourceJobsQueue.getJobs(['delayed']); + + const allJobs = [...waitingJobs, ...activeJobs, ...delayedJobs]; + + for(const source of sources){ + + const jobId = `source-${source.id}`; + + const existingJob = allJobs.find(job => job.name === jobId) + + if (existingJob) { + continue; + } + + await sourceJobsQueue.upsertJobScheduler(jobId, + { + every: source.logFetchInterval * 1000 + }, + { + data: {id: source.id} + } + ) + + logger.info(`Scheduled job for source ${source.id} (Interval: ${source.logFetchInterval}s)`); + } + + } catch (error) + { + logger.error(error) + } +} + +const worker = new Worker( + 'source-jobs', + async job => await handleLogFetch(job), + { connection: redisConnection } + ); + + diff --git a/backend/src/server/background-processing/logger.js b/backend/src/server/background-processing/logger.js new file mode 100644 index 00000000..6797fb61 --- /dev/null +++ b/backend/src/server/background-processing/logger.js @@ -0,0 +1,16 @@ +import pino from 'pino'; + + const logger = pino({ + transport: { + targets: [ + { + target: 'pino-pretty', + options: { colorize: true }, + }, + ], + }, + }); + + + +export default logger; \ No newline at end of file diff --git a/backend/src/server/background-processing/redis-connection.js b/backend/src/server/background-processing/redis-connection.js new file mode 100644 index 00000000..afac0ea0 --- /dev/null +++ b/backend/src/server/background-processing/redis-connection.js @@ -0,0 +1,10 @@ +import Redis from 'ioredis'; + +export const redisConnection = new Redis(process.env.REDIS_PORT, process.env.REDIS_HOST, { + maxRetriesPerRequest: null +}); + +redisConnection.on('error', (err) => { + console.error('Redis connection error:', err); + process.exit(1); +}); diff --git a/backend/src/server/controllers/source.controller.js b/backend/src/server/controllers/source.controller.js new file mode 100644 index 00000000..663b402e --- /dev/null +++ b/backend/src/server/controllers/source.controller.js @@ -0,0 +1,61 @@ +import Source from '../../data/models/Source.js'; + +export const addSource = async (request, reply) => { + const body = request.body; + + const sourceExists = await Source.exists({id:body.id}); + if(sourceExists){ + throw new Error('An item with this id already exists'); + } + + const newSource = new Source({id:body.id, sourceType: body.sourceType, callbackUrl: body.callbackUrl, logFetchInterval: body.logFetchInterval, credentials: JSON.stringify(body.credentials)}); + await newSource.save(); + + return reply.send({ + message:'source added' + }); +}; + + +export const removeSource = async (request, reply) => { + const {id} = request.params; + const source = await Source.findOne({ id }); + + if(!source){ + throw new Error('No source with this id exists'); + } + + await Source.deleteOne({id}) + + return {message: 'source removed'}; +} + +export const getActiveSources = async (request, reply) => { + const sources = await Source.find(); + + const transformedSources = sources.map(source => { + try { + return { + id: source.id, + sourceType: source.sourceType, + logFetchInterval: source.logFetchInterval, + callbackUrl: source.callbackUrl, + // credentials: JSON.parse(source.getDecryptedData()) + } + + } catch { + return { + id: source.id, + sourceType: source.sourceType, + logFetchInterval: source.logFetchInterval, + callbackUrl: source.callbackUrl, + // credentials: source.getDecryptedData() + } + } + }); + + return reply + .code(200) + .header('Content-Type', 'application/json; charset=utf-8') + .send(transformedSources) +}; diff --git a/backend/src/server/middlewares/apiKey.preHandler.js b/backend/src/server/middlewares/apiKey.preHandler.js new file mode 100644 index 00000000..7688f42e --- /dev/null +++ b/backend/src/server/middlewares/apiKey.preHandler.js @@ -0,0 +1,22 @@ +export default async function apiKeyPreHandler(request, reply) { + const publicRoutes = ['/swagger/']; + if (request.method === 'GET' && publicRoutes.some(route => request.url.startsWith(route))) + return; + + const apiKey = request.headers['x-api-key']; + + // you shall not pass + if (!apiKey || apiKey !== process.env.API_KEY) { + reply + .code(401) + .header('Content-Type', 'application/problem+json') + .send({ + type: 'https://example.com/probs/unauthorized', + title: 'Unauthorized', + status: 401, + detail: 'Invalid API Key', + instance: request.url, + }); + } + } + \ No newline at end of file diff --git a/backend/src/server/routes/source.routes.js b/backend/src/server/routes/source.routes.js new file mode 100644 index 00000000..c51eab7a --- /dev/null +++ b/backend/src/server/routes/source.routes.js @@ -0,0 +1,23 @@ +import { addSource, removeSource, getActiveSources } from "../controllers/source.controller.js"; +import { addSourceSchema,deleteSourceSchema,getActiveSourcesSchema } from "../schemas/source.schemas.js"; + +async function sourceRoutes(app) { + app.post('/api/add-source', { + schema: addSourceSchema, + handler: addSource, + }); + + app.delete('/api/delete-source/:id', { + schema: deleteSourceSchema, + handler: removeSource + }); + + app.get('/api/sources', { + schema: getActiveSourcesSchema, + handler: getActiveSources + }); +} + + + +export default sourceRoutes; diff --git a/backend/src/server/schemas/source.schemas.js b/backend/src/server/schemas/source.schemas.js new file mode 100644 index 00000000..11a1b846 --- /dev/null +++ b/backend/src/server/schemas/source.schemas.js @@ -0,0 +1,170 @@ +export const addSourceSchema = { + body: { + type: 'object', + required: ['id', 'sourceType', 'credentials', 'logFetchInterval', 'callbackUrl'], + properties: { + id: { type: 'string', format: 'uuid' }, + sourceType: { type: 'string', enum: ['google_workspace'] }, + credentials: { + type: 'object' + }, + logFetchInterval: { type: 'integer', minimum: 60, }, + callbackUrl: { type: 'string', format: 'uri', }, + }, + }, + description: 'Creates a new Google SDK Source', + tags: ['Source'], + summary: 'Create new source', + response: { + 200: { + description: 'Successful response', + type: 'object', + properties: { + message: { type: 'string' }, + }, + }, + 400: { + description: 'Bad Request', + content: { + 'application/problem+json': { + schema: { + type: 'object', + properties: { + type: { type: 'string', format: 'uri', example: 'https://example.com/probs/invalid-request' }, + title: { type: 'string', example: 'Bad Request' }, + status: { type: 'integer', example: 400 }, + detail: { type: 'string', example: 'Invalid data provided' }, + instance: { type: 'string', example: '/add-source' }, + }, + }, + }, + }, + }, + 500: { + description: 'Internal Server Error', + content: { + 'application/problem+json': { + schema: { + type: 'object', + properties: { + type: { type: 'string', format: 'uri', example: 'https://example.com/probs/internal-server-error' }, + title: { type: 'string', example: 'Internal Server Error' }, + status: { type: 'integer', example: 500 }, + detail: { type: 'string', example: 'Something went wrong on the server' }, + instance: { type: 'string', example: '/add-source' }, + }, + }, + }, + }, + }, +}, +} + +export const deleteSourceSchema = { + description: 'Deletes a source SDK', + tags: ['Source'], + summary: 'Delete a source', + params: { + type: 'object', + required: ['id'], + properties: { + id: { type: 'string', format: 'uuid'}, + }, + }, + response: { + 200: { + description: 'Successful response', + type: 'object', + properties: { + message: { type: 'string' }, + }, + }, + 400: { + description: 'Bad Request', + content: { + 'application/problem+json': { + schema: { + type: 'object', + properties: { + type: { type: 'string', format: 'uri', example: 'https://example.com/probs/invalid-request' }, + title: { type: 'string', example: 'Bad Request' }, + status: { type: 'integer', example: 400 }, + detail: { type: 'string', example: 'Invalid ID format' }, + instance: { type: 'string', example: '/delete-source/{id}' }, + }, + }, + }, + }, + }, + 500: { + description: 'Internal Server Error', + content: { + 'application/problem+json': { + schema: { + type: 'object', + properties: { + type: { type: 'string', format: 'uri', example: 'https://example.com/probs/internal-server-error' }, + title: { type: 'string', example: 'Internal Server Error' }, + status: { type: 'integer', example: 500 }, + detail: { type: 'string', example: 'Something went wrong on the server' }, + instance: { type: 'string', example: '/delete-source/{id}' }, + }, + }, + }, + }, + }, + }, +}; + + + +export const getActiveSourcesSchema = { + description: 'Retrieves active sources. Credentials are hidden for security reasons', + tags: ['Source'], + summary: 'Retrieve active sources', + response: { + 200: { + description: 'Successful response', + type: 'array', + items: { + type: 'object', + additionalProperties: true + }, + }, + + 400: { + description: 'Bad Request', + content: { + 'application/problem+json': { + schema: { + type: 'object', + properties: { + type: { type: 'string', format: 'uri', example: 'https://example.com/probs/invalid-request' }, + title: { type: 'string', example: 'Bad Request' }, + status: { type: 'integer', example: 400 }, + detail: { type: 'string', example: 'Invalid data provided' }, + instance: { type: 'string', example: '/add-source' }, + }, + }, + }, + }, + }, + 500: { + description: 'Internal Server Error', + content: { + 'application/problem+json': { + schema: { + type: 'object', + properties: { + type: { type: 'string', format: 'uri', example: 'https://example.com/probs/internal-server-error' }, + title: { type: 'string', example: 'Internal Server Error' }, + status: { type: 'integer', example: 500 }, + detail: { type: 'string', example: 'Something went wrong on the server' }, + instance: { type: 'string', example: '/add-source' }, + }, + }, + }, + }, + }, + }, +} \ No newline at end of file diff --git a/backend/src/server/seed.js b/backend/src/server/seed.js new file mode 100644 index 00000000..3eec2e97 --- /dev/null +++ b/backend/src/server/seed.js @@ -0,0 +1,31 @@ +import Source from "../data/models/Source.js" + +const seedDb = async () => { + try { + + const newSource = { + id: 'bff552d1-4ac2-45f8-8ffe-a09accfd0d26', + sourceType: 'google_workspace', + credentials: JSON.stringify({ + clientEmail: 'string', + privateKey: 'string', + scopes: ['admin.googleapis.com'] + }), + logFetchInterval: 300, + callbackUrl: process.env.CALLBACK_API_HOOK + }; + + // Check if source exists, if not, insert it + const existingSource = await Source.findOneAndDelete({ id: newSource.id }); + + const source = new Source(newSource); + await source.save(); + console.log('Source saved successfully!'); + + } catch (err) { + console.error('Error:', err); + } + }; + + +export default seedDb; \ No newline at end of file diff --git a/backend/src/server/server.js b/backend/src/server/server.js new file mode 100644 index 00000000..53032344 --- /dev/null +++ b/backend/src/server/server.js @@ -0,0 +1,13 @@ +import app from './app.js'; + +const start = async () => { + try { + await app.listen({ port: 3000 }); + console.log('Server running on http://localhost:3000'); + } catch (err) { + app.log.error(err); + process.exit(1); + } +}; + +start(); diff --git a/backend/src/server/services/index.js b/backend/src/server/services/index.js new file mode 100644 index 00000000..e69de29b diff --git a/backend/src/server/swagger.js b/backend/src/server/swagger.js new file mode 100644 index 00000000..c54df4fe --- /dev/null +++ b/backend/src/server/swagger.js @@ -0,0 +1,31 @@ +import swagger from '@fastify/swagger'; +import swaggerUI from '@fastify/swagger-ui'; + +export const setupSwagger = async (app) => { + await app.register(swagger, { + openapi: { + info: { + title: 'Fastify API', + description: 'API documentation', + version: '1.0.0', + }, + servers: [{ url: 'http://localhost:3000' }], + components: { + securitySchemes: { + ApiKeyAuth: { + type: 'apiKey', + in: 'header', + name: 'x-api-key', + description: 'Enter your API Key to authenticate requests.', + }, + }, + }, + security: [{ ApiKeyAuth: [] }], + }, + }); + + await app.register(swaggerUI, { + routePrefix: '/swagger', + exposeRoute: true, + }); +}; diff --git a/backend/src/server/validations/index.js b/backend/src/server/validations/index.js new file mode 100644 index 00000000..e69de29b From 02e9668c0fb11ac9d7dcf7b2894e47460eefb028 Mon Sep 17 00:00:00 2001 From: Bleron213 Date: Wed, 12 Mar 2025 16:48:58 +0100 Subject: [PATCH 02/22] Update README.md --- README.md | 185 +++++++++++++++++++++++++----------------------------- 1 file changed, 87 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index 3305f704..a64c57a7 100644 --- a/README.md +++ b/README.md @@ -1,118 +1,107 @@ # Backend Challenge: Google Workspace Event Integration -## Introduction -Welcome to the **Cybee.ai Backend Challenge**! +

+
+
+ Backend Challenge Solution +
+

-This challenge will test your ability to **integrate with a cloud event source**, specifically **Google Workspace Admin SDK logs**, and build a system that: -1. **Accepts a new source** (`POST /add-source`) with authentication credentials. -2. **Periodically fetches logs** from Google Workspace. -3. **Processes and forwards logs** to a specified callback URL. -4. **Handles edge cases** like API rate limits, failures, and credential expiration. +

+* ## Key Features -If you complete the challenge successfully, you’ll get a chance to talk with our team at Cybee.ai! +* API and Background Services, utilizing Bull.mq +* Dockerized +* Resilient ---- +* ## Technology Stack -## Tech Stack Requirements -Your solution must be built using: +* Node.js +* Mongo Db +* Redis + +

-- Node.js -- Fastify (for API development) -- MongoDB (for storing sources and logs) -- Redis (for caching and job scheduling) -- Elasticsearch (for log indexing) (optional but a plus) -- Google Workspace Admin SDK (for fetching event logs) +## How To Use -## Requirements +1. Non-dockerized solution -### 1. Build a Secure REST API -Develop a **Fastify-based API** that allows users to connect a cloud event source and receive logs. +* Clone the git repo locally -#### Endpoints -- `POST /add-source` - - Accepts **Google Workspace** as a source type. - - Stores API credentials securely. - - Validates credentials before storing. - -- `DELETE /remove-source/:id` - - Removes an existing event source. - -- `GET /sources` - - Returns a list of active sources. - ---- - -### 2. Source Configuration & Data Model -When a user adds a Google Workspace integration, the system should store: -```json -{ - "id": "uuid", - "sourceType": "google_workspace", - "credentials": { - "clientEmail": "string", - "privateKey": "string", - "scopes": ["admin.googleapis.com"] - }, - "logFetchInterval": 300, - "callbackUrl": "https://example.com/webhook" -} +```bash +# Clone this repository +$ git clone https://github.com/Bleron213/backend-challenge +``` + +# How to run in your local machine + +* Open the folder where the solution was cloned +* Open terminal +* Move to the backend folder +* Inside the backend folder, create a file named .env and place these environment variables inside + +Note: In a production environment, we would never expose API key and Encryption key like this. For demo purposes, this is fine. +# .env file + +API_KEY=bBJ4Gig5CEVzTWM8l2nVCzX8Ht7IohuAFgsKK1puNmGU4FZormELBoRtjPySs4bAX6st4VOO2Vx8CSxoiQQuzWrrhEWlw2mwF17Boo5hun9Wo0RZZGhgsoK7uXSBD8AR +MONGO_URI=mongodb://localhost:27017/sourcedb +ENCRYPTION_KEY=0d932b4a920075ca6bd78fb589b9815d878b1bd06fbf1f7477b69102e8967908 +REDIS_PORT=6379 +REDIS_HOST=localhost +NODE_DEBUG=bull +NODE_ENV=DEVELOPMENT +CALLBACK_API_HOOK=http://localhost:8080/Hooks/SendLog + +* Run these three docker commands + +```bash +$ docker run --name mongodb -d -p 27017:27017 mongo +$ docker run --name redis-server -p 6379:6379 -d redis +$ docker run -d -p 8080:8080 --name callbackapi-container -e ASPNETCORE_ENVIRONMENT=Development bleronqorri/callbackapi:latest +``` + +* Run npm install and npm start + +```bash +# npm install +``` + +and + +```bash +# npm start ``` -**Notes:** -- Credentials should be **stored securely** (e.g., encrypted in MongoDB). -- `logFetchInterval` defines how often logs should be fetched (in seconds). -- `callbackUrl` is where processed logs should be sent. - ---- - -### 3. Fetch & Forward Logs Automatically -- Once a source is added, the system should: - - **Schedule a job** to fetch logs at `logFetchInterval` (e.g., using a queue like BullMQ). - - Call **Google Workspace Admin SDK** (`Reports API`) to fetch **audit logs**. - - **Forward logs** to the `callbackUrl` of the source. - - **Retry failed requests** and handle rate limits. - -**Example Log from Google Workspace:** -```json -{ - "id": "log-id", - "timestamp": "2024-03-10T12:00:00Z", - "actor": { - "email": "admin@example.com", - "ipAddress": "192.168.1.1" - }, - "eventType": "LOGIN", - "details": { - "status": "SUCCESS" - } -} + +* Backend challenge should now be up and running. You can inspect the console to see logs. Alternatively, you can connect to MongoDb to view data inside the sourcedb and you can connect to Redis Insights to view the Job scheduling inside Redis. + +2. Dockerized solution + +* Clone the git repo locally + +```bash +# Clone this repository +$ git clone https://github.com/Bleron213/backend-challenge ``` ---- +# How to run in your local machine -### 4. Handle Edge Cases -Your system should properly handle: -**API rate limits** – Backoff and retry. -**Credential expiration** – Detect and alert the user. -**Callback failures** – Retry failed webhook deliveries. -**Duplicate logs** – Ensure logs are not duplicated. -**High availability** – Ensure logs keep flowing even if one instance restarts. +* Open the folder where the solution was cloned +* Move to the backend folder +* Inside the backend folder, create a file named .env and place these environment variables inside ---- +Note: In a production environment, we would never expose API key and Encryption key like this. For demo purposes, this is fine. -### 5. Deployment & Bonus -- (Required) Provide a **README** with: - - Setup instructions. - - API documentation. - - Explanation of how retries and scheduling work. -- (Bonus) Deploy the solution using **Docker & a cloud provider**. -- (Bonus) Implement **monitoring** (e.g., log metrics to Elasticsearch). +# .env file ---- +API_KEY=bBJ4Gig5CEVzTWM8l2nVCzX8Ht7IohuAFgsKK1puNmGU4FZormELBoRtjPySs4bAX6st4VOO2Vx8CSxoiQQuzWrrhEWlw2mwF17Boo5hun9Wo0RZZGhgsoK7uXSBD8AR +ENCRYPTION_KEY=0d932b4a920075ca6bd78fb589b9815d878b1bd06fbf1f7477b69102e8967908 +NODE_ENV=DEVELOPMENT -## How to Submit -1. Fork this repository and implement your solution in a `backend/` folder. -2. Add a `README.md` with setup and usage instructions. -3. Submit a pull request. +* Inside the backend folder, open terminal and run -If your solution meets the challenge requirements, we’ll reach out to schedule a conversation. Looking forward to seeing your work! +```bash +# Clone this repository +$ docker-compose up +``` +* Backend challenge should now be up and running. You can inspect the console to see logs. Alternatively, you can connect to MongoDb to view data inside the sourcedb and you can connect to Redis Insights to view the Job scheduling inside Redis. From 4b1e6daad45428751c0ebb4f35f2b90db2c0c95c Mon Sep 17 00:00:00 2001 From: Bleron213 Date: Wed, 12 Mar 2025 16:54:59 +0100 Subject: [PATCH 03/22] Update README.md --- README.md | 88 +++++++++++++++++++++++++++---------------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index a64c57a7..70b86478 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,43 @@ # Backend Challenge: Google Workspace Event Integration

-
-
- Backend Challenge Solution -
+ Backend Challenge Solution

-* ## Key Features - -* API and Background Services, utilizing Bull.mq -* Dockerized -* Resilient - -* ## Technology Stack + Key Features:
+ * API and Background Services, utilizing Bull.mq
+ * Dockerized
+ * Resilient
+

-* Node.js -* Mongo Db -* Redis - +

+ Technology Stack:
+ * Node.js
+ * MongoDB
+ * Redis

+--- + ## How To Use -1. Non-dockerized solution +### 1. Non-Dockerized Solution -* Clone the git repo locally +#### Clone the Repository ```bash # Clone this repository $ git clone https://github.com/Bleron213/backend-challenge ``` -# How to run in your local machine - -* Open the folder where the solution was cloned -* Open terminal -* Move to the backend folder -* Inside the backend folder, create a file named .env and place these environment variables inside +#### Setting Up the Local Environment -Note: In a production environment, we would never expose API key and Encryption key like this. For demo purposes, this is fine. -# .env file +1. Open the folder where the solution was cloned. +2. Open a terminal and move to the `backend` folder. +3. Inside the `backend` folder, create a file named `.env` and place these environment variables inside: +```dotenv API_KEY=bBJ4Gig5CEVzTWM8l2nVCzX8Ht7IohuAFgsKK1puNmGU4FZormELBoRtjPySs4bAX6st4VOO2Vx8CSxoiQQuzWrrhEWlw2mwF17Boo5hun9Wo0RZZGhgsoK7uXSBD8AR MONGO_URI=mongodb://localhost:27017/sourcedb ENCRYPTION_KEY=0d932b4a920075ca6bd78fb589b9815d878b1bd06fbf1f7477b69102e8967908 @@ -50,9 +45,10 @@ REDIS_PORT=6379 REDIS_HOST=localhost NODE_DEBUG=bull NODE_ENV=DEVELOPMENT -CALLBACK_API_HOOK=http://localhost:8080/Hooks/SendLog +CALLBACK_API_HOOK=http://localhost:8080/Hooks/SendLog +``` -* Run these three docker commands +4. Run these Docker commands: ```bash $ docker run --name mongodb -d -p 27017:27017 mongo @@ -60,48 +56,52 @@ $ docker run --name redis-server -p 6379:6379 -d redis $ docker run -d -p 8080:8080 --name callbackapi-container -e ASPNETCORE_ENVIRONMENT=Development bleronqorri/callbackapi:latest ``` -* Run npm install and npm start +5. Install dependencies and start the backend: ```bash # npm install +$ npm install ``` -and - ```bash # npm start +$ npm start ``` -* Backend challenge should now be up and running. You can inspect the console to see logs. Alternatively, you can connect to MongoDb to view data inside the sourcedb and you can connect to Redis Insights to view the Job scheduling inside Redis. +The backend challenge should now be up and running. You can inspect the console to see logs. Alternatively, you can connect to MongoDB to view data inside the `sourcedb` and connect to Redis Insights to view the job scheduling inside Redis. -2. Dockerized solution +### 2. Dockerized Solution -* Clone the git repo locally +#### Clone the Repository ```bash # Clone this repository $ git clone https://github.com/Bleron213/backend-challenge ``` -# How to run in your local machine +#### Setting Up the Local Environment -* Open the folder where the solution was cloned -* Move to the backend folder -* Inside the backend folder, create a file named .env and place these environment variables inside - -Note: In a production environment, we would never expose API key and Encryption key like this. For demo purposes, this is fine. - -# .env file +1. Open the folder where the solution was cloned. +2. Open a terminal and move to the `backend` folder. +3. Inside the `backend` folder, create a file named `.env` and place these environment variables inside: +```dotenv API_KEY=bBJ4Gig5CEVzTWM8l2nVCzX8Ht7IohuAFgsKK1puNmGU4FZormELBoRtjPySs4bAX6st4VOO2Vx8CSxoiQQuzWrrhEWlw2mwF17Boo5hun9Wo0RZZGhgsoK7uXSBD8AR ENCRYPTION_KEY=0d932b4a920075ca6bd78fb589b9815d878b1bd06fbf1f7477b69102e8967908 NODE_ENV=DEVELOPMENT +``` -* Inside the backend folder, open terminal and run +4. Start the Docker containers: ```bash -# Clone this repository +# Start the containers with Docker Compose $ docker-compose up ``` -* Backend challenge should now be up and running. You can inspect the console to see logs. Alternatively, you can connect to MongoDb to view data inside the sourcedb and you can connect to Redis Insights to view the Job scheduling inside Redis. +The backend challenge should now be up and running. You can inspect the console to see logs. Alternatively, you can connect to MongoDB to view data inside the `sourcedb` and connect to Redis Insights to view the job scheduling inside Redis. + +--- + +## Notes + +- In a production environment, we would never expose API keys or encryption keys like this. For demo purposes, this is fine. From 8e4bc4a6ad38fd107ae3a176da59ab8acaf42b53 Mon Sep 17 00:00:00 2001 From: Bleron213 Date: Wed, 12 Mar 2025 16:55:43 +0100 Subject: [PATCH 04/22] Update README.md --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 70b86478..2acf0a7f 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,15 @@ Backend Challenge Solution -

Key Features:
* API and Background Services, utilizing Bull.mq
* Dockerized
* Resilient
-

-

Technology Stack:
* Node.js
* MongoDB
* Redis
-

--- From 8af9a57fee18d755e2a88e1133b6c8b17c57a5ea Mon Sep 17 00:00:00 2001 From: Bleron213 Date: Wed, 12 Mar 2025 17:00:28 +0100 Subject: [PATCH 05/22] Update README.md --- README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/README.md b/README.md index 2acf0a7f..3cde9574 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ + +

# Backend Challenge: Google Workspace Event Integration +

Backend Challenge Solution @@ -66,6 +69,31 @@ $ npm start The backend challenge should now be up and running. You can inspect the console to see logs. Alternatively, you can connect to MongoDB to view data inside the `sourcedb` and connect to Redis Insights to view the job scheduling inside Redis. +1. Swagger documentation for endpoints + +Open http://localhost:3000/swagger/ and view the documented endpoints. +To use them, you need to provide the API key defined in .env variables. +We have exposed it here - but in a production environment, this would be the first layer of security. + +![image](https://github.com/user-attachments/assets/85d4b55b-7925-469a-a31a-b597b0bd9d8d) + +2. Redis Insight + +We can also view job schedules internals in Redis Insights. +Open up Redis insights and connect to the redis running in the Docker container + +![image](https://github.com/user-attachments/assets/1e476a03-a521-4472-b5bd-c71f2b2e22ea) + +3. MongoDb + +We can also view created sources and logs in db +Open up Mongo Compass and connect to Mongo db running in docker + +We can see the following info + +![image](https://github.com/user-attachments/assets/8cdb36d7-75e9-4b24-9f3a-c86f674ccfac) + + ### 2. Dockerized Solution #### Clone the Repository From 6571adf1374e7a8cdd314301fa95f0a403559f0c Mon Sep 17 00:00:00 2001 From: Bleron213 Date: Wed, 12 Mar 2025 17:06:40 +0100 Subject: [PATCH 06/22] Update README.md --- README.md | 56 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 3cde9574..b3d86fd0 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,22 @@ - -

# Backend Challenge: Google Workspace Event Integration -

Backend Challenge Solution

+

Key Features:
* API and Background Services, utilizing Bull.mq
* Dockerized
* Resilient
+

+

Technology Stack:
* Node.js
* MongoDB
* Redis
+

--- @@ -69,30 +70,37 @@ $ npm start The backend challenge should now be up and running. You can inspect the console to see logs. Alternatively, you can connect to MongoDB to view data inside the `sourcedb` and connect to Redis Insights to view the job scheduling inside Redis. -1. Swagger documentation for endpoints +#### 1. Swagger documentation for endpoints -Open http://localhost:3000/swagger/ and view the documented endpoints. -To use them, you need to provide the API key defined in .env variables. +Open http://localhost:3000/swagger/ and view the documented endpoints. +To use them, you need to provide the API key defined in `.env` variables. We have exposed it here - but in a production environment, this would be the first layer of security. -![image](https://github.com/user-attachments/assets/85d4b55b-7925-469a-a31a-b597b0bd9d8d) +![Swagger](https://github.com/user-attachments/assets/85d4b55b-7925-469a-a31a-b597b0bd9d8d) + +#### 2. Redis Insight -2. Redis Insight +We can also view job schedules internals in Redis Insights. +Open Redis Insights and connect to the Redis running in the Docker container. -We can also view job schedules internals in Redis Insights. -Open up Redis insights and connect to the redis running in the Docker container +![Redis Insights](https://github.com/user-attachments/assets/1e476a03-a521-4472-b5bd-c71f2b2e22ea) -![image](https://github.com/user-attachments/assets/1e476a03-a521-4472-b5bd-c71f2b2e22ea) +#### 3. MongoDB -3. MongoDb +We can also view created sources and logs in the database. +Open Mongo Compass and connect to MongoDB running in Docker. -We can also view created sources and logs in db -Open up Mongo Compass and connect to Mongo db running in docker +We can see the following info: -We can see the following info +![MongoDB](https://github.com/user-attachments/assets/8cdb36d7-75e9-4b24-9f3a-c86f674ccfac) -![image](https://github.com/user-attachments/assets/8cdb36d7-75e9-4b24-9f3a-c86f674ccfac) +#### 4. Resilience in Action +![Resilience Logs](https://github.com/user-attachments/assets/23b52a9a-f5e6-4c0a-a12c-ff87dee9a704) + +Here we can see logs being processed. Due to the aggressive rate limiter in the callback API, retries will be quite common. + +--- ### 2. Dockerized Solution @@ -124,6 +132,22 @@ $ docker-compose up The backend challenge should now be up and running. You can inspect the console to see logs. Alternatively, you can connect to MongoDB to view data inside the `sourcedb` and connect to Redis Insights to view the job scheduling inside Redis. +#### 1. Swagger documentation for endpoints + +![Swagger](https://github.com/user-attachments/assets/85d4b55b-7925-469a-a31a-b597b0bd9d8d) + +#### 2. Redis Insight + +![Redis Insights](https://github.com/user-attachments/assets/1e476a03-a521-4472-b5bd-c71f2b2e22ea) + +#### 3. MongoDB + +![MongoDB](https://github.com/user-attachments/assets/8cdb36d7-75e9-4b24-9f3a-c86f674ccfac) + +#### 4. Resilience in Action + +![Resilience Logs](https://github.com/user-attachments/assets/23b52a9a-f5e6-4c0a-a12c-ff87dee9a704) + --- ## Notes From 31f2da92145dd2bd98f8c4cb77744c28555c34b2 Mon Sep 17 00:00:00 2001 From: Bleron213 Date: Wed, 12 Mar 2025 17:35:53 +0100 Subject: [PATCH 07/22] Update README.md --- README.md | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index b3d86fd0..f5818926 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,15 @@ Backend Challenge Solution

-

- Key Features:
- * API and Background Services, utilizing Bull.mq
- * Dockerized
- * Resilient
-

- -

- Technology Stack:
- * Node.js
- * MongoDB
- * Redis
-

+Key Features:
+* API and Background Services, utilizing Bull.mq
+* Dockerized
+* Resilient
+ +Technology Stack:
+* Node.js
+* MongoDB
+* Redis
--- From 48d79a63f1b52ff2bb40c96565ce7e995c9a2e9b Mon Sep 17 00:00:00 2001 From: Bleron213 Date: Wed, 12 Mar 2025 17:36:17 +0100 Subject: [PATCH 08/22] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index f5818926..391c3ad0 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ REDIS_HOST=localhost NODE_DEBUG=bull NODE_ENV=DEVELOPMENT CALLBACK_API_HOOK=http://localhost:8080/Hooks/SendLog +DOCKER=0 ``` 4. Run these Docker commands: @@ -117,6 +118,7 @@ $ git clone https://github.com/Bleron213/backend-challenge API_KEY=bBJ4Gig5CEVzTWM8l2nVCzX8Ht7IohuAFgsKK1puNmGU4FZormELBoRtjPySs4bAX6st4VOO2Vx8CSxoiQQuzWrrhEWlw2mwF17Boo5hun9Wo0RZZGhgsoK7uXSBD8AR ENCRYPTION_KEY=0d932b4a920075ca6bd78fb589b9815d878b1bd06fbf1f7477b69102e8967908 NODE_ENV=DEVELOPMENT +DOCKER=1 ``` 4. Start the Docker containers: From 5cdc88296ac56803fbba5092c6d1f073388e271f Mon Sep 17 00:00:00 2001 From: Bleron213 Date: Wed, 12 Mar 2025 17:38:13 +0100 Subject: [PATCH 09/22] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 391c3ad0..cc9dc95e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -# Backend Challenge: Google Workspace Event Integration -

Backend Challenge Solution

From d53bd8cc6807f9823cf63279e6a9fb53a996af2d Mon Sep 17 00:00:00 2001 From: Bleron Qorri Date: Wed, 12 Mar 2025 17:46:51 +0100 Subject: [PATCH 10/22] Updated some paths, and exposed dockerized api --- backend/.gitignore => .gitignore | 0 backend/docker-compose.yml | 4 ++-- backend/src/config/index.js | 2 -- backend/src/server/app.js | 1 + backend/src/server/middlewares/apiKey.preHandler.js | 4 +++- backend/src/server/server.js | 10 ++++++++-- 6 files changed, 14 insertions(+), 7 deletions(-) rename backend/.gitignore => .gitignore (100%) diff --git a/backend/.gitignore b/.gitignore similarity index 100% rename from backend/.gitignore rename to .gitignore diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 8348aacf..1b05779c 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -14,10 +14,10 @@ services: - REDIS_PORT=6379 - NODE_DEBUG=bull - NODE_ENV=${NODE_ENV} + - DOCKER=1 - CALLBACK_API_HOOK=http://callbackapi:8080/Hooks/SendLog ports: - - 3000:3000 - - 9229:9229 + - "3000:3000" command: ["npm", "start"] depends_on: - mongo diff --git a/backend/src/config/index.js b/backend/src/config/index.js index 6826643a..bfe22e0f 100644 --- a/backend/src/config/index.js +++ b/backend/src/config/index.js @@ -1,9 +1,7 @@ import dotenv from 'dotenv'; -// loads environment variables from .env into process.env. Makes our app configurable. dotenv.config(); -// we are running on development const env = process.env.NODE_ENV || 'development'; module.exports = { diff --git a/backend/src/server/app.js b/backend/src/server/app.js index 58031925..329e7d91 100644 --- a/backend/src/server/app.js +++ b/backend/src/server/app.js @@ -36,6 +36,7 @@ await setupSwagger(app); await app.after(); + app.addHook('preHandler', apiKeyPreHandler); app.register(AutoLoad, { diff --git a/backend/src/server/middlewares/apiKey.preHandler.js b/backend/src/server/middlewares/apiKey.preHandler.js index 7688f42e..e5b26eef 100644 --- a/backend/src/server/middlewares/apiKey.preHandler.js +++ b/backend/src/server/middlewares/apiKey.preHandler.js @@ -1,5 +1,7 @@ export default async function apiKeyPreHandler(request, reply) { - const publicRoutes = ['/swagger/']; + + console.log(request.url) + const publicRoutes = ['/swagger']; if (request.method === 'GET' && publicRoutes.some(route => request.url.startsWith(route))) return; diff --git a/backend/src/server/server.js b/backend/src/server/server.js index 53032344..03b831fc 100644 --- a/backend/src/server/server.js +++ b/backend/src/server/server.js @@ -2,8 +2,14 @@ import app from './app.js'; const start = async () => { try { - await app.listen({ port: 3000 }); - console.log('Server running on http://localhost:3000'); + + if(process.env.DOCKER && process.env.DOCKER === 1){ + await app.listen({ port: 3000, host: '0.0.0.0'}); + } else { + await app.listen({ port: 3000}); + console.log('Server running on http://localhost:3000'); + } + } catch (err) { app.log.error(err); process.exit(1); From be7bf7f4daa1f38c2f28b793e243e8870cf95bdc Mon Sep 17 00:00:00 2001 From: Bleron213 Date: Thu, 13 Mar 2025 01:03:01 +0100 Subject: [PATCH 11/22] Update README.md --- README.md | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index cc9dc95e..5115e91a 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,7 @@ #### Clone the Repository ```bash -# Clone this repository -$ git clone https://github.com/Bleron213/backend-challenge +git clone https://github.com/Bleron213/backend-challenge ``` #### Setting Up the Local Environment @@ -46,21 +45,26 @@ DOCKER=0 4. Run these Docker commands: ```bash -$ docker run --name mongodb -d -p 27017:27017 mongo -$ docker run --name redis-server -p 6379:6379 -d redis -$ docker run -d -p 8080:8080 --name callbackapi-container -e ASPNETCORE_ENVIRONMENT=Development bleronqorri/callbackapi:latest +docker run --name mongodb -d -p 27017:27017 mongo +docker run --name redis-server -p 6379:6379 -d redis +docker run -d -p 8080:8080 --name callbackapi-container -e ASPNETCORE_ENVIRONMENT=Development bleronqorri/callbackapi:latest + +``` + +Note: callbackapi-container might have issues on mac. If it doesn't work, please use the following command + +```bash +docker run -d --platform linux/amd64 -p 8080:8080 --name callbackapi-container -e ASPNETCORE_ENVIRONMENT=Development bleronqorri/callbackapi:latest ``` 5. Install dependencies and start the backend: ```bash -# npm install -$ npm install +npm install ``` ```bash -# npm start -$ npm start +npm start ``` The backend challenge should now be up and running. You can inspect the console to see logs. Alternatively, you can connect to MongoDB to view data inside the `sourcedb` and connect to Redis Insights to view the job scheduling inside Redis. From 527eba1332cfcd472d96caaab92f1185ccb1c252 Mon Sep 17 00:00:00 2001 From: Bleron213 Date: Thu, 13 Mar 2025 01:10:06 +0100 Subject: [PATCH 12/22] Update README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 5115e91a..acec93f8 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,12 @@ DOCKER=1 $ docker-compose up ``` +Note: callbackapi-container might have issues on mac. If it doesn't work, please include the platform on callbackapi in docker-compose.yml + +```bash + platform: linux/amd64 +``` + The backend challenge should now be up and running. You can inspect the console to see logs. Alternatively, you can connect to MongoDB to view data inside the `sourcedb` and connect to Redis Insights to view the job scheduling inside Redis. #### 1. Swagger documentation for endpoints From 049e2a1cc5392a4fb68e5735cffe4345df6c5e50 Mon Sep 17 00:00:00 2001 From: Bleron Qorri Date: Thu, 13 Mar 2025 01:34:16 +0100 Subject: [PATCH 13/22] fixed DOCKER string and number comparison --- backend/src/server/server.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/src/server/server.js b/backend/src/server/server.js index 03b831fc..28d161fd 100644 --- a/backend/src/server/server.js +++ b/backend/src/server/server.js @@ -3,10 +3,14 @@ import app from './app.js'; const start = async () => { try { - if(process.env.DOCKER && process.env.DOCKER === 1){ + if(process.env.DOCKER && process.env.DOCKER === "1"){ + console.log(`running on docker. Listening on port 3000. `) await app.listen({ port: 3000, host: '0.0.0.0'}); - } else { + } + else + { await app.listen({ port: 3000}); + console.log('running locally.') console.log('Server running on http://localhost:3000'); } From 0d116418d0bd49c9e2441cc01ca8290636d81864 Mon Sep 17 00:00:00 2001 From: Bleron213 Date: Thu, 13 Mar 2025 01:35:51 +0100 Subject: [PATCH 14/22] Update README.md --- README.md | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index acec93f8..9bf6da3d 100644 --- a/README.md +++ b/README.md @@ -51,12 +51,6 @@ docker run -d -p 8080:8080 --name callbackapi-container -e ASPNETCORE_ENVIRONMEN ``` -Note: callbackapi-container might have issues on mac. If it doesn't work, please use the following command - -```bash -docker run -d --platform linux/amd64 -p 8080:8080 --name callbackapi-container -e ASPNETCORE_ENVIRONMENT=Development bleronqorri/callbackapi:latest -``` - 5. Install dependencies and start the backend: ```bash @@ -130,12 +124,6 @@ DOCKER=1 $ docker-compose up ``` -Note: callbackapi-container might have issues on mac. If it doesn't work, please include the platform on callbackapi in docker-compose.yml - -```bash - platform: linux/amd64 -``` - The backend challenge should now be up and running. You can inspect the console to see logs. Alternatively, you can connect to MongoDB to view data inside the `sourcedb` and connect to Redis Insights to view the job scheduling inside Redis. #### 1. Swagger documentation for endpoints @@ -159,3 +147,14 @@ The backend challenge should now be up and running. You can inspect the console ## Notes - In a production environment, we would never expose API keys or encryption keys like this. For demo purposes, this is fine. +- callbackapi-container might have issues on mac. If it doesn't work, please use the following command (if locally) + +```bash +docker run -d --platform linux/amd64 -p 8080:8080 --name callbackapi-container -e ASPNETCORE_ENVIRONMENT=Development bleronqorri/callbackapi:latest +``` + +or if using docker compose, include platform in callbackapi settings + +```bash + platform: linux/amd64 +``` From 25971a88a9f5bc957a7594c47f682863036c3f43 Mon Sep 17 00:00:00 2001 From: Bleron213 Date: Thu, 13 Mar 2025 08:44:38 +0100 Subject: [PATCH 15/22] Update README.md --- README.md | 48 ++++++++++++++++++------------------------------ 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 9bf6da3d..b09ae5a0 100644 --- a/README.md +++ b/README.md @@ -63,36 +63,6 @@ npm start The backend challenge should now be up and running. You can inspect the console to see logs. Alternatively, you can connect to MongoDB to view data inside the `sourcedb` and connect to Redis Insights to view the job scheduling inside Redis. -#### 1. Swagger documentation for endpoints - -Open http://localhost:3000/swagger/ and view the documented endpoints. -To use them, you need to provide the API key defined in `.env` variables. -We have exposed it here - but in a production environment, this would be the first layer of security. - -![Swagger](https://github.com/user-attachments/assets/85d4b55b-7925-469a-a31a-b597b0bd9d8d) - -#### 2. Redis Insight - -We can also view job schedules internals in Redis Insights. -Open Redis Insights and connect to the Redis running in the Docker container. - -![Redis Insights](https://github.com/user-attachments/assets/1e476a03-a521-4472-b5bd-c71f2b2e22ea) - -#### 3. MongoDB - -We can also view created sources and logs in the database. -Open Mongo Compass and connect to MongoDB running in Docker. - -We can see the following info: - -![MongoDB](https://github.com/user-attachments/assets/8cdb36d7-75e9-4b24-9f3a-c86f674ccfac) - -#### 4. Resilience in Action - -![Resilience Logs](https://github.com/user-attachments/assets/23b52a9a-f5e6-4c0a-a12c-ff87dee9a704) - -Here we can see logs being processed. Due to the aggressive rate limiter in the callback API, retries will be quite common. - --- ### 2. Dockerized Solution @@ -126,22 +96,40 @@ $ docker-compose up The backend challenge should now be up and running. You can inspect the console to see logs. Alternatively, you can connect to MongoDB to view data inside the `sourcedb` and connect to Redis Insights to view the job scheduling inside Redis. +--- + +## Seein everything in action + #### 1. Swagger documentation for endpoints +Open http://localhost:3000/swagger/ and view the documented endpoints. +To use them, you need to provide the API key defined in `.env` variables. +We have exposed it here - but in a production environment, this would be the first layer of security. + ![Swagger](https://github.com/user-attachments/assets/85d4b55b-7925-469a-a31a-b597b0bd9d8d) #### 2. Redis Insight +We can also view job schedules internals in Redis Insights. +Open Redis Insights and connect to the Redis running in the Docker container. + ![Redis Insights](https://github.com/user-attachments/assets/1e476a03-a521-4472-b5bd-c71f2b2e22ea) #### 3. MongoDB +We can also view created sources and logs in the database. +Open Mongo Compass and connect to MongoDB running in Docker. + +We can see the following info: + ![MongoDB](https://github.com/user-attachments/assets/8cdb36d7-75e9-4b24-9f3a-c86f674ccfac) #### 4. Resilience in Action ![Resilience Logs](https://github.com/user-attachments/assets/23b52a9a-f5e6-4c0a-a12c-ff87dee9a704) +Here we can see logs being processed. Due to the aggressive rate limiter in the callback API, retries will be quite common. + --- ## Notes From 149ffd5ab858680d8860d86b8548a9cd741c4708 Mon Sep 17 00:00:00 2001 From: Bleron Qorri Date: Thu, 13 Mar 2025 15:18:20 +0100 Subject: [PATCH 16/22] restructuring of bull.mq jobs for clarity --- backend/package-lock.json | 380 +++++++++++++++++- backend/package.json | 1 + backend/src/data/models/Source.js | 1 + backend/src/server/app.js | 4 +- .../server/background-processing/bullmq.js | 33 +- .../callback-api-handler.js | 128 ------ .../{ => config}/redis-connection.js | 0 .../jobs/callback-api-handler-job.js | 152 +++++++ .../jobs/credential-expiration-job.js | 46 +++ .../log-fetching-scheduler-job.js} | 27 +- .../queues/log-fetching-queue.js | 7 + .../queues/log-fetching-scheduler-queue.js | 23 ++ .../queues/source-queue.js | 18 + .../workers/log-fetching-scheduler-worker.js | 6 + .../workers/log-fetching-worker.js | 12 + .../workers/source-jobs-worker.js | 20 + .../server/controllers/source.controller.js | 21 +- backend/src/server/schemas/source.schemas.js | 12 +- backend/src/server/seed.js | 21 +- .../src/server/utils/credentials.service.js | 58 +++ backend/src/server/utils/email.js | 27 ++ .../logger.js | 3 +- 22 files changed, 810 insertions(+), 190 deletions(-) delete mode 100644 backend/src/server/background-processing/callback-api-handler.js rename backend/src/server/background-processing/{ => config}/redis-connection.js (100%) create mode 100644 backend/src/server/background-processing/jobs/callback-api-handler-job.js create mode 100644 backend/src/server/background-processing/jobs/credential-expiration-job.js rename backend/src/server/background-processing/{log-fetching-scheduler.js => jobs/log-fetching-scheduler-job.js} (53%) create mode 100644 backend/src/server/background-processing/queues/log-fetching-queue.js create mode 100644 backend/src/server/background-processing/queues/log-fetching-scheduler-queue.js create mode 100644 backend/src/server/background-processing/queues/source-queue.js create mode 100644 backend/src/server/background-processing/workers/log-fetching-scheduler-worker.js create mode 100644 backend/src/server/background-processing/workers/log-fetching-worker.js create mode 100644 backend/src/server/background-processing/workers/source-jobs-worker.js create mode 100644 backend/src/server/utils/credentials.service.js create mode 100644 backend/src/server/utils/email.js rename backend/src/server/{background-processing => utils}/logger.js (80%) diff --git a/backend/package-lock.json b/backend/package-lock.json index 29db4b80..42a32a76 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -18,6 +18,7 @@ "bullmq": "^5.41.8", "dotenv": "^16.4.7", "fastify": "^5.2.1", + "googleapis": "^146.0.0", "ioredis": "^5.6.0", "mongoose": "^8.12.1", "pino": "^9.6.0", @@ -544,6 +545,15 @@ "node": ">= 0.6" } }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -682,6 +692,35 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -758,6 +797,12 @@ "node": ">=16.20.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/bullmq": { "version": "5.41.8", "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.8.tgz", @@ -801,7 +846,6 @@ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", - "peer": true, "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" @@ -1091,6 +1135,15 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1241,6 +1294,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-copy": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", @@ -1520,6 +1579,110 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gcp-metadata": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", + "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gcp-metadata/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/gcp-metadata/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/gcp-metadata/node_modules/gaxios": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", + "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gcp-metadata/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gcp-metadata/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1591,6 +1754,76 @@ "node": ">= 6" } }, + "node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-auth-library/node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis": { + "version": "146.0.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-146.0.0.tgz", + "integrity": "sha512-NewqvhnBZOJsugCAOo636O0BGE/xY7Cg/v8Rjm1+5LkJCjcqAzLleJ6igd5vrRExJLSKrY9uHy9iKE7r0PrfhQ==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^9.0.0", + "googleapis-common": "^7.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/googleapis-common": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz", + "integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^6.0.3", + "google-auth-library": "^9.7.0", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1603,6 +1836,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -1675,6 +1921,42 @@ "node": ">= 0.8" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1836,6 +2118,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1879,6 +2173,15 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-schema-ref-resolver": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-2.0.1.tgz", @@ -1944,6 +2247,27 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kareem": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", @@ -2325,6 +2649,48 @@ "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", "license": "MIT" }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-gyp-build-optional-packages": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", @@ -2409,7 +2775,6 @@ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" }, @@ -2661,7 +3026,6 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "side-channel": "^1.0.6" }, @@ -2960,7 +3324,6 @@ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "license": "MIT", - "peer": true, "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", @@ -2980,7 +3343,6 @@ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "license": "MIT", - "peer": true, "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" @@ -2997,7 +3359,6 @@ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -3016,7 +3377,6 @@ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -3381,6 +3741,12 @@ "node": ">= 0.8" } }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", + "license": "BSD" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", diff --git a/backend/package.json b/backend/package.json index d1ecdb08..352a4547 100644 --- a/backend/package.json +++ b/backend/package.json @@ -18,6 +18,7 @@ "bullmq": "^5.41.8", "dotenv": "^16.4.7", "fastify": "^5.2.1", + "googleapis": "^146.0.0", "ioredis": "^5.6.0", "mongoose": "^8.12.1", "pino": "^9.6.0", diff --git a/backend/src/data/models/Source.js b/backend/src/data/models/Source.js index e1402706..0ba45682 100644 --- a/backend/src/data/models/Source.js +++ b/backend/src/data/models/Source.js @@ -35,6 +35,7 @@ const sourceSchema = new mongoose.Schema({ }, }, credentials: { type: String, required: true }, + expired: {type: Boolean, default: false}, }, { timestamps: true }); sourceSchema.pre('save', function (next) { diff --git a/backend/src/server/app.js b/backend/src/server/app.js index 329e7d91..a22cd289 100644 --- a/backend/src/server/app.js +++ b/backend/src/server/app.js @@ -5,9 +5,11 @@ import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import apiKeyPreHandler from './middlewares/apiKey.preHandler.js'; import connectDB from '../data/models/db.js'; -import './background-processing/bullmq.js' import seedDb from './seed.js'; +import './background-processing/bullmq.js' + + const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); diff --git a/backend/src/server/background-processing/bullmq.js b/backend/src/server/background-processing/bullmq.js index c959ebc1..2a4ec587 100644 --- a/backend/src/server/background-processing/bullmq.js +++ b/backend/src/server/background-processing/bullmq.js @@ -1,29 +1,6 @@ -import { Queue, Worker } from 'bullmq'; -import {scheduleLogFetchingJobs} from './log-fetching-scheduler.js' -import { redisConnection } from './redis-connection.js'; +import './queues/log-fetching-scheduler-queue.js' +import './queues/source-queue.js' - -export const logFetchingSchedulerQueue = new Queue('log-fetching-scheduler', { connection: redisConnection }); - -await logFetchingSchedulerQueue.upsertJobScheduler( - 'check-source-entries', - { - every: 3000, - }, - { - data: {}, - opts: { - removeOnComplete: true - }, - } - ); - - new Worker( - 'log-fetching-scheduler', - async job => await scheduleLogFetchingJobs(job), - { connection: redisConnection } - ); - - - -export const sourceJobsQueue = new Queue('source-jobs', { connection: redisConnection }); \ No newline at end of file +import './workers/log-fetching-scheduler-worker.js' +import './workers/source-jobs-worker.js' +import './workers/log-fetching-worker.js'; \ No newline at end of file diff --git a/backend/src/server/background-processing/callback-api-handler.js b/backend/src/server/background-processing/callback-api-handler.js deleted file mode 100644 index ada5034b..00000000 --- a/backend/src/server/background-processing/callback-api-handler.js +++ /dev/null @@ -1,128 +0,0 @@ -import Source from "../../data/models/Source.js"; -import axios from "axios"; -import axiosRetry from "axios-retry"; -import logger from "./logger.js"; -import Log from "../../data/models/Log.js"; - -axiosRetry(axios, { - retries: 3, - retryCondition: (error) => { - return error.response && ( - error.response.status === 429 || - error.response.status === 503 || - error.response.status === 500 || - error.response.status === 400 - ); - }, - retryDelay: (retryCount, error) => { - if (error.response && error.response.status === 429) { - const retryAfter = error.response.headers['retry-after']; - if (retryAfter) { - logger.info(`Retrying after ${parseInt(retryAfter)} milliseconds`) - return parseInt(retryAfter); - } - } - return Math.pow(2, retryCount) * 1000; - }, - onRetry: (retryCount, error, requestConfig) => { - var data = JSON.parse(requestConfig.data); - logger.warn(`Encountered error: ${error.message}. Retrying... Attempt ${retryCount} for request: ${data.id}`); - return; - }, - onMaxRetryTimesExceeded: async (error, retryCount) => { - logger.error(error, 'Max retries exceeded for request'); - var log = JSON.parse(error.config.data); - - await Log.updateOne( - { id: log.id }, - { $set: - { - id: log.id, - retryCount: retryCount, - message: error.message, - status:'failed' - } - }, - { upsert: true } - ); - - return; - } - -}); - -export const handleLogFetch = async (job) => { - const {id} = job.data; - - var source = await Source.findOne({id: id}); - - if(!source){ - logger.warn('no source found.') - job.remove(); - } - - logger.info(`processing source...id: ${id}`); - - // call google api - // let's pretend api returned successfully. logs are my deserialized response - const logs = generateLogs(); - - for (const log of logs) { - try { - const dbLog = Log.findOne({id:log.id}); - - if(dbLog && dbLog.status === 'successful') { - logger.info(`Log with id = ${dbLog.id} has already been processed before`); - continue; - } - - // add additional cases. For logs that have failed over three times, we may want a different handle. For now, let's just retry them. - - logger.info('sending request...' + log.id); - const response = await axios.post(source.callbackUrl, log); - logger.info('received response'); - if(response.status === 200){ - logger.info(`log with id ${log.id} was successfully processed`); - await Log.updateOne( - { id: log.id }, - { $set: - { - id: log.id, - payload: log, - status:'successful' - } - }, - { upsert: true } - ); - - } - } catch (error){ - logger.error(error, `log with id ${log.id} was not processed successfully`); - } - } - - logger.info('success!'); -} - - // to-do: remove once unblocked - const generateLogs = () => { - let logs = []; - - for (let i = 0; i < 5000; i++) { - const log = { - id: `log-id-${i + 1}`, - timestamp: new Date().toISOString(), // Current timestamp - actor: { - email: `user${Math.floor(Math.random() * 1000)}@example.com`, // Random email - ipAddress: `${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}` // Random IP address - }, - eventType: Math.random() > 0.5 ? 'LOGIN' : 'LOGOUT', // Random LOGIN or LOGOUT event - details: { - status: Math.random() > 0.5 ? 'SUCCESS' : 'FAILURE' // Random status: SUCCESS or FAILURE - } - }; - logs.push(log); - } - - return logs; - } \ No newline at end of file diff --git a/backend/src/server/background-processing/redis-connection.js b/backend/src/server/background-processing/config/redis-connection.js similarity index 100% rename from backend/src/server/background-processing/redis-connection.js rename to backend/src/server/background-processing/config/redis-connection.js diff --git a/backend/src/server/background-processing/jobs/callback-api-handler-job.js b/backend/src/server/background-processing/jobs/callback-api-handler-job.js new file mode 100644 index 00000000..2096fed4 --- /dev/null +++ b/backend/src/server/background-processing/jobs/callback-api-handler-job.js @@ -0,0 +1,152 @@ +import Source from "../../../data/models/Source.js"; +import axios from "axios"; +import axiosRetry from "axios-retry"; +import logger from "../../utils/logger.js"; +import Log from "../../../data/models/Log.js"; +import {checkSourceCredentialsExpired} from '../../utils/credentials.service.js' + +axiosRetry(axios, { + retries: 3, + retryCondition: (error) => { + return error.response && ( + error.response.status === 429 || + error.response.status === 503 || + error.response.status === 500 || + error.response.status === 400 + ); + }, + retryDelay: (retryCount, error) => { + if (error.response && error.response.status === 429) { + const retryAfter = error.response.headers['retry-after']; + if (retryAfter) { + logger.info(`Retrying after ${parseInt(retryAfter)} milliseconds`) + return parseInt(retryAfter); + } + } + return Math.pow(2, retryCount) * 1000; + }, + onRetry: (retryCount, error, requestConfig) => { + var data = JSON.parse(requestConfig.data); + logger.warn(`Encountered error: ${error.message}. Retrying... Attempt ${retryCount} for request: ${data.id}`); + return; + }, + onMaxRetryTimesExceeded: async (error, retryCount) => { + logger.error(error, 'Max retries exceeded for request'); + var log = JSON.parse(error.config.data); + + await Log.updateOne( + { id: log.id }, + { $set: + { + id: log.id, + retryCount: retryCount, + message: error.message, + status:'failed' + } + }, + { upsert: true } + ); + + return; + } + +}); + +export const handleLogFetch = async (job) => { + try { + const {id} = job.data; + + var source = await Source.findOne({id: id}); + + if(!source){ + logger.warn('no source found.') + job.remove(); + } + + logger.info(`processing source...id: ${id}`); + + // call google api + // validate credentials first + // ignore my own special id for now + if(source.id !== 'bff552d1-4ac2-45f8-8ffe-a09accfd0d26'){ + const result = await checkSourceCredentialsExpired(source); + + if (result.error) { + await Source.updateOne( + { id: source.id }, + { + $set: { expired: true } + } + ); + return; + } + } + + + // let's pretend api returned successfully. logs are my deserialized response + const logs = generateLogs(); + + for (const log of logs) { + try { + const dbLog = Log.findOne({id:log.id}); + + if(dbLog && dbLog.status === 'successful') { + logger.info(`Log with id = ${dbLog.id} has already been processed before`); + continue; + } + + // add additional cases. For logs that have failed over three times, we may want a different handle. For now, let's just retry them. + + logger.info('sending request...' + log.id); + const response = await axios.post(source.callbackUrl, log); + logger.info('received response'); + if(response.status === 200){ + logger.info(`log with id ${log.id} was successfully processed`); + await Log.updateOne( + { id: log.id }, + { $set: + { + id: log.id, + payload: log, + status:'successful' + } + }, + { upsert: true } + ); + + } + } catch (error){ + logger.error(error, `log with id ${log.id} was not processed successfully`); + } + } + + logger.info('success!'); + } + catch (error) { + logger.error(error); + } + + } + + // to-do: remove once unblocked + const generateLogs = () => { + let logs = []; + + for (let i = 0; i < 5000; i++) { + const log = { + id: `log-id-${i + 1}`, + timestamp: new Date().toISOString(), // Current timestamp + actor: { + email: `user${Math.floor(Math.random() * 1000)}@example.com`, // Random email + ipAddress: `${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}` // Random IP address + }, + eventType: Math.random() > 0.5 ? 'LOGIN' : 'LOGOUT', // Random LOGIN or LOGOUT event + details: { + status: Math.random() > 0.5 ? 'SUCCESS' : 'FAILURE' // Random status: SUCCESS or FAILURE + } + }; + logs.push(log); + } + + return logs; + } \ No newline at end of file diff --git a/backend/src/server/background-processing/jobs/credential-expiration-job.js b/backend/src/server/background-processing/jobs/credential-expiration-job.js new file mode 100644 index 00000000..a8917b91 --- /dev/null +++ b/backend/src/server/background-processing/jobs/credential-expiration-job.js @@ -0,0 +1,46 @@ +import Source from "../../../data/models/Source.js" +import { Worker } from "bullmq" +import { redisConnection } from '../config/redis-connection.js'; +import logger from '../../utils/logger.js'; +import {checkSourceCredentialsExpired} from '../../utils/credentials.service.js' + +export const CREDENTIAL_EXPIRATION_JOB_NAME = "credentialsExpirationJob"; +// eslint-disable-next-line no-unused-vars +export const credentialsExpirationJob = async (job) => { + try { + + logger.info('checking credentials...'); + // make an exception for my test credentials + const sources = await Source.find({ + expired: false, + id: { $ne: "bff552d1-4ac2-45f8-8ffe-a09accfd0d26" } + }); + + if(sources.length === 0){ + return; + } + + for(const source of sources){ + + const result = await checkSourceCredentialsExpired(source); + + if (result.error) { + await Source.updateOne( + { id: source.id }, + { + $set: { expired: true } + } + ); + } + } + + + } + catch (error) { + logger.error(error) + } +} + + + + diff --git a/backend/src/server/background-processing/log-fetching-scheduler.js b/backend/src/server/background-processing/jobs/log-fetching-scheduler-job.js similarity index 53% rename from backend/src/server/background-processing/log-fetching-scheduler.js rename to backend/src/server/background-processing/jobs/log-fetching-scheduler-job.js index 7d6e6ab9..b5fdc2f4 100644 --- a/backend/src/server/background-processing/log-fetching-scheduler.js +++ b/backend/src/server/background-processing/jobs/log-fetching-scheduler-job.js @@ -1,11 +1,8 @@ -import Source from "../../data/models/Source.js" -import {sourceJobsQueue, logFetchingSchedulerQueue} from './bullmq.js' -import { Worker } from "bullmq" -import { redisConnection } from './redis-connection.js'; -import axios from "axios"; -import { handleLogFetch } from "./callback-api-handler.js"; -import logger from './logger.js'; +import Source from "../../../data/models/Source.js" +import logger from '../../utils/logger.js'; +import { logFetchingQueue } from "../queues/log-fetching-queue.js"; +// eslint-disable-next-line no-unused-vars export const scheduleLogFetchingJobs = async (job) => { try{ @@ -16,15 +13,15 @@ export const scheduleLogFetchingJobs = async (job) => { return; } - const waitingJobs = await sourceJobsQueue.getJobs(['waiting']); - const activeJobs = await sourceJobsQueue.getJobs(['active']); - const delayedJobs = await sourceJobsQueue.getJobs(['delayed']); + const waitingJobs = await logFetchingQueue.getJobs(['waiting']); + const activeJobs = await logFetchingQueue.getJobs(['active']); + const delayedJobs = await logFetchingQueue.getJobs(['delayed']); const allJobs = [...waitingJobs, ...activeJobs, ...delayedJobs]; for(const source of sources){ - const jobId = `source-${source.id}`; + const jobId = `process-source-logs-${source.id}`; const existingJob = allJobs.find(job => job.name === jobId) @@ -32,7 +29,7 @@ export const scheduleLogFetchingJobs = async (job) => { continue; } - await sourceJobsQueue.upsertJobScheduler(jobId, + await logFetchingQueue.upsertJobScheduler(jobId, { every: source.logFetchInterval * 1000 }, @@ -50,10 +47,4 @@ export const scheduleLogFetchingJobs = async (job) => { } } -const worker = new Worker( - 'source-jobs', - async job => await handleLogFetch(job), - { connection: redisConnection } - ); - diff --git a/backend/src/server/background-processing/queues/log-fetching-queue.js b/backend/src/server/background-processing/queues/log-fetching-queue.js new file mode 100644 index 00000000..26c57084 --- /dev/null +++ b/backend/src/server/background-processing/queues/log-fetching-queue.js @@ -0,0 +1,7 @@ + +import { redisConnection } from '../config/redis-connection.js'; +import { Queue, Worker } from 'bullmq'; + +export const LOG_FETCHING_QUEUE_NAME = 'log-fetching-queue'; + +export const logFetchingQueue = new Queue(LOG_FETCHING_QUEUE_NAME, { connection: redisConnection }); \ No newline at end of file diff --git a/backend/src/server/background-processing/queues/log-fetching-scheduler-queue.js b/backend/src/server/background-processing/queues/log-fetching-scheduler-queue.js new file mode 100644 index 00000000..0f98e031 --- /dev/null +++ b/backend/src/server/background-processing/queues/log-fetching-scheduler-queue.js @@ -0,0 +1,23 @@ + +import { redisConnection } from '../config/redis-connection.js'; +import { Queue, Worker } from 'bullmq'; + +export const LOG_FETCHING_SCHEDULER_QUEUE_NAME = 'log-fetching-scheduler-queue'; + +export const logFetchingSchedulerQueue = new Queue(LOG_FETCHING_SCHEDULER_QUEUE_NAME, { connection: redisConnection }); + +(async () => { + await logFetchingSchedulerQueue.upsertJobScheduler( + 'check-source-entries', + { + every: 3000, + }, + { + data: {}, + opts: { + removeOnComplete: true + }, + } + ); + })(); + diff --git a/backend/src/server/background-processing/queues/source-queue.js b/backend/src/server/background-processing/queues/source-queue.js new file mode 100644 index 00000000..ed46065e --- /dev/null +++ b/backend/src/server/background-processing/queues/source-queue.js @@ -0,0 +1,18 @@ + +import { redisConnection } from '../config/redis-connection.js'; +import { Queue, Worker } from 'bullmq'; +import { CREDENTIAL_EXPIRATION_JOB_NAME } from '../jobs/credential-expiration-job.js'; + +export const SOURCE_JOBS_QUEUE_NAME = 'source-jobs-queue'; + +export const sourceJobsQueue = new Queue(SOURCE_JOBS_QUEUE_NAME, { connection: redisConnection }); + +(async () => { + await sourceJobsQueue.add( + CREDENTIAL_EXPIRATION_JOB_NAME, + {}, + { + repeat: { cron: '0 */6 * * *' } + } + ); + })(); \ No newline at end of file diff --git a/backend/src/server/background-processing/workers/log-fetching-scheduler-worker.js b/backend/src/server/background-processing/workers/log-fetching-scheduler-worker.js new file mode 100644 index 00000000..f7d45cb0 --- /dev/null +++ b/backend/src/server/background-processing/workers/log-fetching-scheduler-worker.js @@ -0,0 +1,6 @@ +import { Worker } from 'bullmq'; +import { redisConnection } from '../config/redis-connection.js'; +import { LOG_FETCHING_SCHEDULER_QUEUE_NAME } from '../queues/log-fetching-scheduler-queue.js'; +import { scheduleLogFetchingJobs } from '../jobs/log-fetching-scheduler-job.js'; + +export const worker = new Worker(LOG_FETCHING_SCHEDULER_QUEUE_NAME,async job => await scheduleLogFetchingJobs(job),{ connection: redisConnection }); \ No newline at end of file diff --git a/backend/src/server/background-processing/workers/log-fetching-worker.js b/backend/src/server/background-processing/workers/log-fetching-worker.js new file mode 100644 index 00000000..30618274 --- /dev/null +++ b/backend/src/server/background-processing/workers/log-fetching-worker.js @@ -0,0 +1,12 @@ +import { Worker } from 'bullmq'; +import { redisConnection } from '../config/redis-connection.js'; +import { LOG_FETCHING_QUEUE_NAME } from '../queues/log-fetching-queue.js'; +import { handleLogFetch } from '../jobs/callback-api-handler-job.js'; + +export const worker1 = new Worker(LOG_FETCHING_QUEUE_NAME,async job => { + if (job.name.startsWith('process-source-logs-')) { + await handleLogFetch(job); + return; + } +},{ connection: redisConnection, concurrency: 3 }); + diff --git a/backend/src/server/background-processing/workers/source-jobs-worker.js b/backend/src/server/background-processing/workers/source-jobs-worker.js new file mode 100644 index 00000000..a5191215 --- /dev/null +++ b/backend/src/server/background-processing/workers/source-jobs-worker.js @@ -0,0 +1,20 @@ +import { CREDENTIAL_EXPIRATION_JOB_NAME, credentialsExpirationJob } from "../jobs/credential-expiration-job.js"; +import {SOURCE_JOBS_QUEUE_NAME} from '../queues/source-queue.js' +import { handleLogFetch } from "../jobs/callback-api-handler-job.js"; +import { Worker } from 'bullmq'; +import { redisConnection } from '../config/redis-connection.js'; + +export const worker = new Worker( + SOURCE_JOBS_QUEUE_NAME, + async job => { + if (job.name === CREDENTIAL_EXPIRATION_JOB_NAME) { + await credentialsExpirationJob(job); + return; + } + else { + console.log(`Unknown job type: ${job.name}`); + } + }, + { connection: redisConnection } + ); + diff --git a/backend/src/server/controllers/source.controller.js b/backend/src/server/controllers/source.controller.js index 663b402e..930667fc 100644 --- a/backend/src/server/controllers/source.controller.js +++ b/backend/src/server/controllers/source.controller.js @@ -1,11 +1,23 @@ import Source from '../../data/models/Source.js'; +import { checkCredentialsExpired } from '../utils/credentials.service.js'; export const addSource = async (request, reply) => { const body = request.body; - const sourceExists = await Source.exists({id:body.id}); - if(sourceExists){ - throw new Error('An item with this id already exists'); + const sourceExists = await Source.exists({ id: body.id }); + if (sourceExists) { + return reply.code(409).send({ + message: 'Source already exists' + }); + } + + // validate credentials before storing them + const validated = await checkCredentialsExpired(body.credentials) + if(!validated.error){ + return reply.code(400).send({ + message: 'Credentials don`t appear to be valid', + error: validated.error + }); } const newSource = new Source({id:body.id, sourceType: body.sourceType, callbackUrl: body.callbackUrl, logFetchInterval: body.logFetchInterval, credentials: JSON.stringify(body.credentials)}); @@ -17,6 +29,7 @@ export const addSource = async (request, reply) => { }; +// eslint-disable-next-line no-unused-vars export const removeSource = async (request, reply) => { const {id} = request.params; const source = await Source.findOne({ id }); @@ -40,7 +53,6 @@ export const getActiveSources = async (request, reply) => { sourceType: source.sourceType, logFetchInterval: source.logFetchInterval, callbackUrl: source.callbackUrl, - // credentials: JSON.parse(source.getDecryptedData()) } } catch { @@ -49,7 +61,6 @@ export const getActiveSources = async (request, reply) => { sourceType: source.sourceType, logFetchInterval: source.logFetchInterval, callbackUrl: source.callbackUrl, - // credentials: source.getDecryptedData() } } }); diff --git a/backend/src/server/schemas/source.schemas.js b/backend/src/server/schemas/source.schemas.js index 11a1b846..b413f18a 100644 --- a/backend/src/server/schemas/source.schemas.js +++ b/backend/src/server/schemas/source.schemas.js @@ -6,7 +6,17 @@ export const addSourceSchema = { id: { type: 'string', format: 'uuid' }, sourceType: { type: 'string', enum: ['google_workspace'] }, credentials: { - type: 'object' + type: 'object', + required: ['clientEmail', 'privateKey', 'scopes'], + properties: { + clientEmail: { type: 'string', format: 'email' }, + privateKey: { type: 'string' }, + scopes: { + type: 'array', + items: { type: 'string' }, + minItems: 1 + } + } }, logFetchInterval: { type: 'integer', minimum: 60, }, callbackUrl: { type: 'string', format: 'uri', }, diff --git a/backend/src/server/seed.js b/backend/src/server/seed.js index 3eec2e97..04588b24 100644 --- a/backend/src/server/seed.js +++ b/backend/src/server/seed.js @@ -16,11 +16,30 @@ const seedDb = async () => { }; // Check if source exists, if not, insert it - const existingSource = await Source.findOneAndDelete({ id: newSource.id }); + await Source.findOneAndDelete({ id: newSource.id }); const source = new Source(newSource); await source.save(); console.log('Source saved successfully!'); + + const expireSource = { + id: '0e40ca89-cd9a-4c21-b131-f3b0e4ea4666', + sourceType: 'google_workspace', + credentials: JSON.stringify({ + clientEmail: 'string', + privateKey: 'string', + scopes: ['admin.googleapis.com'] + }), + logFetchInterval: 300, + callbackUrl: process.env.CALLBACK_API_HOOK + }; + + await Source.findOneAndDelete({ id: expireSource.id }); + + const expireSourceEntity = new Source(expireSource); + await expireSourceEntity.save(); + console.log('Source saved successfully!'); + } catch (err) { console.error('Error:', err); diff --git a/backend/src/server/utils/credentials.service.js b/backend/src/server/utils/credentials.service.js new file mode 100644 index 00000000..48b34097 --- /dev/null +++ b/backend/src/server/utils/credentials.service.js @@ -0,0 +1,58 @@ +import { google } from 'googleapis'; +import logger from './logger.js' + +export const checkSourceCredentialsExpired = async (source) => { + try { + const credentials = JSON.parse(source.getDecryptedData()); + + const auth = new google.auth.GoogleAuth({ + credentials: { + client_email: credentials.clientEmail, + private_key: credentials.privateKey + }, + scopes: credentials.scopes + }); + + const client = await auth.getClient(); + await client.getAccessToken(); + + // So far, credentials look valid! + return { + error: '', + validated: true + }; + } catch (error) { + logger.error(error) + return { + error: error.message, + validated: false + }; + } +} + +export const checkCredentialsExpired = async (credentials) => { + try { + const auth = new google.auth.GoogleAuth({ + credentials: { + client_email: credentials.clientEmail, + private_key: credentials.privateKey + }, + scopes: credentials.scopes + }); + + const client = await auth.getClient(); + await client.getAccessToken(); + + // So far, credentials look valid! + return { + error: '', + validated: true + }; + } catch (error) { + logger.error(error) + return { + error: error.message, + validated: false + }; + } +} \ No newline at end of file diff --git a/backend/src/server/utils/email.js b/backend/src/server/utils/email.js new file mode 100644 index 00000000..9002b9b1 --- /dev/null +++ b/backend/src/server/utils/email.js @@ -0,0 +1,27 @@ +import { createTransport } from "nodemailer"; +import logger from "server/utils/logger"; + +export const credentialsExpirationEmail = async (id, email) => { + let transporter = createTransport({ + service: "gmail", + auth: { + user: "bleron213@gmail.com", + pass: "gmail-integration-password" + } + }); + + let mailOptions = { + from: "bleron213@gmail.com", + to: email, + subject: "Your credentials have expired", + text: `Your credentials with id = ${id} have expired`, + html: `Your credentials with id = ${id} have expired` + }; + + try { + let info = await transporter.sendMail(mailOptions); + logger.log("Email sent:", info.response); + } catch (error) { + logger.error("Error sending email:", error); + } +} diff --git a/backend/src/server/background-processing/logger.js b/backend/src/server/utils/logger.js similarity index 80% rename from backend/src/server/background-processing/logger.js rename to backend/src/server/utils/logger.js index 6797fb61..ee16c8f7 100644 --- a/backend/src/server/background-processing/logger.js +++ b/backend/src/server/utils/logger.js @@ -4,8 +4,9 @@ import pino from 'pino'; transport: { targets: [ { + level: 'info', target: 'pino-pretty', - options: { colorize: true }, + options: {} }, ], }, From 9ae58b7f14fbee77337720d887d21cb04edfdbe9 Mon Sep 17 00:00:00 2001 From: Bleron Qorri Date: Thu, 13 Mar 2025 16:23:31 +0100 Subject: [PATCH 17/22] added alerting of the user in case creds have expired --- .../background-processing/jobs/callback-api-handler-job.js | 6 ++++++ .../jobs/credential-expiration-job.js | 7 +++++++ backend/src/server/controllers/source.controller.js | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/backend/src/server/background-processing/jobs/callback-api-handler-job.js b/backend/src/server/background-processing/jobs/callback-api-handler-job.js index 2096fed4..6f2b9626 100644 --- a/backend/src/server/background-processing/jobs/callback-api-handler-job.js +++ b/backend/src/server/background-processing/jobs/callback-api-handler-job.js @@ -4,6 +4,7 @@ import axiosRetry from "axios-retry"; import logger from "../../utils/logger.js"; import Log from "../../../data/models/Log.js"; import {checkSourceCredentialsExpired} from '../../utils/credentials.service.js' +import { credentialsExpirationEmail } from "../../utils/email.js"; axiosRetry(axios, { retries: 3, @@ -78,6 +79,11 @@ export const handleLogFetch = async (job) => { $set: { expired: true } } ); + const credentials = source.getDecryptedData(); + + if(credentials && credentials.clientEmail){ + await credentialsExpirationEmail(source.id, credentials.clientEmail) + } return; } } diff --git a/backend/src/server/background-processing/jobs/credential-expiration-job.js b/backend/src/server/background-processing/jobs/credential-expiration-job.js index a8917b91..25f1761b 100644 --- a/backend/src/server/background-processing/jobs/credential-expiration-job.js +++ b/backend/src/server/background-processing/jobs/credential-expiration-job.js @@ -3,6 +3,7 @@ import { Worker } from "bullmq" import { redisConnection } from '../config/redis-connection.js'; import logger from '../../utils/logger.js'; import {checkSourceCredentialsExpired} from '../../utils/credentials.service.js' +import { credentialsExpirationEmail } from "../../utils/email.js"; export const CREDENTIAL_EXPIRATION_JOB_NAME = "credentialsExpirationJob"; // eslint-disable-next-line no-unused-vars @@ -31,6 +32,12 @@ export const credentialsExpirationJob = async (job) => { $set: { expired: true } } ); + + const credentials = source.getDecryptedData(); + + if(credentials && credentials.clientEmail){ + await credentialsExpirationEmail(source.id, credentials.clientEmail) + } } } diff --git a/backend/src/server/controllers/source.controller.js b/backend/src/server/controllers/source.controller.js index 930667fc..6b8491fb 100644 --- a/backend/src/server/controllers/source.controller.js +++ b/backend/src/server/controllers/source.controller.js @@ -44,7 +44,7 @@ export const removeSource = async (request, reply) => { } export const getActiveSources = async (request, reply) => { - const sources = await Source.find(); + const sources = await Source.find({ expired: false }); const transformedSources = sources.map(source => { try { From ab041841f0988077c3ecaf5927a8567008a3b219 Mon Sep 17 00:00:00 2001 From: Bleron Qorri Date: Fri, 14 Mar 2025 10:43:57 +0100 Subject: [PATCH 18/22] resolved missing package and wrong path --- backend/package-lock.json | 10 ++++++++++ backend/package.json | 1 + backend/src/server/utils/email.js | 2 +- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 42a32a76..0806b84b 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -21,6 +21,7 @@ "googleapis": "^146.0.0", "ioredis": "^5.6.0", "mongoose": "^8.12.1", + "nodemailer": "^6.10.0", "pino": "^9.6.0", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1" @@ -2706,6 +2707,15 @@ "node-gyp-build-optional-packages-test": "build-test.js" } }, + "node_modules/nodemailer": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz", + "integrity": "sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", diff --git a/backend/package.json b/backend/package.json index 352a4547..481feea7 100644 --- a/backend/package.json +++ b/backend/package.json @@ -21,6 +21,7 @@ "googleapis": "^146.0.0", "ioredis": "^5.6.0", "mongoose": "^8.12.1", + "nodemailer": "^6.10.0", "pino": "^9.6.0", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1" diff --git a/backend/src/server/utils/email.js b/backend/src/server/utils/email.js index 9002b9b1..8f738bd9 100644 --- a/backend/src/server/utils/email.js +++ b/backend/src/server/utils/email.js @@ -1,5 +1,5 @@ import { createTransport } from "nodemailer"; -import logger from "server/utils/logger"; +import logger from "./logger.js"; export const credentialsExpirationEmail = async (id, email) => { let transporter = createTransport({ From 3aade3a64c84c12f855829974e472118de13c6ea Mon Sep 17 00:00:00 2001 From: Bleron Qorri Date: Fri, 14 Mar 2025 19:20:58 +0100 Subject: [PATCH 19/22] added support for elastic --- backend/docker-compose.yml | 23 ++ backend/package-lock.json | 376 +++++++++++++++++- backend/package.json | 3 + backend/src/data/models/db.js | 2 +- backend/src/server/app.js | 33 +- .../jobs/callback-api-handler-job.js | 6 +- .../jobs/credential-expiration-job.js | 4 +- .../jobs/log-fetching-scheduler-job.js | 4 +- .../src/server/utils/credentials.service.js | 2 +- backend/src/server/utils/email.js | 2 +- backend/src/server/utils/logger.js | 17 - backend/src/server/utils/logging/logger.js | 73 ++++ 12 files changed, 500 insertions(+), 45 deletions(-) delete mode 100644 backend/src/server/utils/logger.js create mode 100644 backend/src/server/utils/logging/logger.js diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 1b05779c..4a67d89b 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -16,12 +16,14 @@ services: - NODE_ENV=${NODE_ENV} - DOCKER=1 - CALLBACK_API_HOOK=http://callbackapi:8080/Hooks/SendLog + - ELASTIC_SEARCH_NODE=http://elasticsearch:9200 ports: - "3000:3000" command: ["npm", "start"] depends_on: - mongo - redis-server + - elasticsearch networks: - backend-challenge-network @@ -55,6 +57,27 @@ services: networks: - backend-challenge-network + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:8.3.3 + environment: + - discovery.type=single-node + # Elasticsearch 8.x has HTTPS and auth on by default. This option is + # needed to use HTTP and no auth (as used in the tests) + # for demo purposes, this is fine. + - xpack.security.enabled=false + container_name: elasticsearch + ports: ['9200:9200'] + networks: + - backend-challenge-network + + kibana: + image: docker.elastic.co/kibana/kibana:8.3.3 + container_name: kibana + ports: ['5601:5601'] + depends_on: ['elasticsearch'] + networks: + - backend-challenge-network + volumes: mongo_data: redis_data: diff --git a/backend/package-lock.json b/backend/package-lock.json index 0806b84b..2b57f522 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,6 +9,8 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@elastic/ecs-pino-format": "^1.5.0", + "@elastic/elasticsearch": "^8.17.1", "@fastify/autoload": "^6.2.0", "@fastify/swagger": "^9.4.2", "@fastify/swagger-ui": "^5.2.2", @@ -23,6 +25,7 @@ "mongoose": "^8.12.1", "nodemailer": "^6.10.0", "pino": "^9.6.0", + "pino-elasticsearch": "^8.1.0", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1" }, @@ -75,6 +78,82 @@ "openapi-types": ">=7" } }, + "node_modules/@elastic/ecs-helpers": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@elastic/ecs-helpers/-/ecs-helpers-2.1.1.tgz", + "integrity": "sha512-ItoNazMnYdlUCmkBYTXc3SG6PF7UlVTbvMdHPvXkfTMPdwGv2G1Xtp5CjDHaGHGOZSwaDrW4RSCXvA/lMSU+rg==", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/@elastic/ecs-pino-format": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@elastic/ecs-pino-format/-/ecs-pino-format-1.5.0.tgz", + "integrity": "sha512-7MMVmT50ucEl7no8mUgCIl+pffBVNRl36uZi0vmalWa2xPWISBxM9k9WSP/WTgOkmGj9G35e5g3UfCS1zxshBg==", + "license": "Apache-2.0", + "dependencies": { + "@elastic/ecs-helpers": "^2.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@elastic/elasticsearch": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-8.17.1.tgz", + "integrity": "sha512-EaDP4/jfNu0nhnHZjxk9bL9ofKWKX9QUdEJ8QsGa+/KMPBEwD+HMyYXH4FSRlg7YONI0UbdO/mMZobvcEnMFBA==", + "license": "Apache-2.0", + "dependencies": { + "@elastic/transport": "^8.9.1", + "apache-arrow": "^18.0.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@elastic/transport": { + "version": "8.9.4", + "resolved": "https://registry.npmjs.org/@elastic/transport/-/transport-8.9.4.tgz", + "integrity": "sha512-y6kjy5s0MQE3MQx9ItmvQ8th7GlGcZfzZ7ZDvI8bUhaKua2dJk01k9ia/bdJ4dnPpWpOyFTRgkgBZS31ZTLpcg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "1.x", + "debug": "^4.3.7", + "hpagent": "^1.2.0", + "ms": "^2.1.3", + "secure-json-parse": "^3.0.1", + "tslib": "^2.8.1", + "undici": "^6.21.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@elastic/transport/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@elastic/transport/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/@fastify/accept-negotiator": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.0.1.tgz", @@ -498,6 +577,15 @@ "win32" ] }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@scarf/scarf": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", @@ -505,12 +593,42 @@ "hasInstallScript": true, "license": "Apache-2.0" }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/command-line-args": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.3.tgz", + "integrity": "sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==", + "license": "MIT" + }, + "node_modules/@types/command-line-usage": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/command-line-usage/-/command-line-usage-5.0.4.tgz", + "integrity": "sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==", + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "license": "MIT" }, + "node_modules/@types/node": { + "version": "20.17.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.24.tgz", + "integrity": "sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -626,12 +744,41 @@ "node": ">= 8" } }, + "node_modules/apache-arrow": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/apache-arrow/-/apache-arrow-18.1.0.tgz", + "integrity": "sha512-v/ShMp57iBnBp4lDgV8Jx3d3Q5/Hac25FWmQ98eMahUiHPXcvwIMKJD0hBIgclm/FCG+LwPkAKtkRO1O/W0YGg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.11", + "@types/command-line-args": "^5.2.3", + "@types/command-line-usage": "^5.0.4", + "@types/node": "^20.13.0", + "command-line-args": "^5.2.1", + "command-line-usage": "^7.0.1", + "flatbuffers": "^24.3.25", + "json-bignum": "^0.0.3", + "tslib": "^2.6.2" + }, + "bin": { + "arrow2csv": "bin/arrow2csv.js" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, + "node_modules/array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -864,6 +1011,73 @@ "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", "license": "MIT" }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk-template": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", + "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/chalk-template?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -935,6 +1149,54 @@ "node": ">= 0.8" } }, + "node_modules/command-line-args": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", + "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", + "license": "MIT", + "dependencies": { + "array-back": "^3.1.0", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/command-line-usage": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.3.tgz", + "integrity": "sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==", + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "chalk-template": "^0.4.0", + "table-layout": "^4.1.0", + "typical": "^7.1.1" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/command-line-usage/node_modules/array-back": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", + "integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/command-line-usage/node_modules/typical": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz", + "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, "node_modules/commander": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", @@ -1172,7 +1434,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -1479,6 +1740,24 @@ "node": ">=14" } }, + "node_modules/find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "license": "MIT", + "dependencies": { + "array-back": "^3.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/flatbuffers": { + "version": "24.12.23", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-24.12.23.tgz", + "integrity": "sha512-dLVCAISd5mhls514keQzmEG6QHmUUsNuWsb4tFafIUwvvgDjXhtfAYSKOzt5SWOy+qByV5pbsDZ+Vb7HUOBEdA==", + "license": "Apache-2.0" + }, "node_modules/follow-redirects": { "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", @@ -1906,6 +2185,15 @@ "dev": true, "license": "MIT" }, + "node_modules/hpagent": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz", + "integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -2183,6 +2471,14 @@ "bignumber.js": "^9.0.0" } }, + "node_modules/json-bignum": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/json-bignum/-/json-bignum-0.0.3.tgz", + "integrity": "sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/json-schema-ref-resolver": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-2.0.1.tgz", @@ -2308,6 +2604,12 @@ "node": ">=18" } }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -2453,7 +2755,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2930,6 +3231,21 @@ "split2": "^4.0.0" } }, + "node_modules/pino-elasticsearch": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/pino-elasticsearch/-/pino-elasticsearch-8.1.0.tgz", + "integrity": "sha512-noIBaNfEGPglMcZDP2aE3/e4bL1AqS05vmbftNA6ml7f9yaEfv4CEe1PRGr0CxwjYG3/rOAPXgzFDbfPH1GKcg==", + "license": "MIT", + "dependencies": { + "@elastic/elasticsearch": "^8.13.1", + "minimist": "^1.2.8", + "pump": "^3.0.0", + "split2": "^4.2.0" + }, + "bin": { + "pino-elasticsearch": "cli.js" + } + }, "node_modules/pino-pretty": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.0.0.tgz", @@ -3015,7 +3331,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "dev": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -3652,6 +3967,28 @@ "express": ">=4.0.0 || >=5.0.0-beta" } }, + "node_modules/table-layout": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz", + "integrity": "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==", + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "wordwrapjs": "^5.1.0" + }, + "engines": { + "node": ">=12.17" + } + }, + "node_modules/table-layout/node_modules/array-back": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", + "integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, "node_modules/thread-stream": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", @@ -3734,6 +4071,15 @@ "node": ">= 0.6" } }, + "node_modules/typical": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -3741,6 +4087,21 @@ "dev": true, "license": "MIT" }, + "node_modules/undici": { + "version": "6.21.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.2.tgz", + "integrity": "sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -3836,6 +4197,15 @@ "node": ">= 8" } }, + "node_modules/wordwrapjs": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-5.1.0.tgz", + "integrity": "sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", diff --git a/backend/package.json b/backend/package.json index 481feea7..a728df4f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -9,6 +9,8 @@ "license": "ISC", "type": "module", "dependencies": { + "@elastic/ecs-pino-format": "^1.5.0", + "@elastic/elasticsearch": "^8.17.1", "@fastify/autoload": "^6.2.0", "@fastify/swagger": "^9.4.2", "@fastify/swagger-ui": "^5.2.2", @@ -23,6 +25,7 @@ "mongoose": "^8.12.1", "nodemailer": "^6.10.0", "pino": "^9.6.0", + "pino-elasticsearch": "^8.1.0", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1" }, diff --git a/backend/src/data/models/db.js b/backend/src/data/models/db.js index 8fff56c3..08089ed9 100644 --- a/backend/src/data/models/db.js +++ b/backend/src/data/models/db.js @@ -8,7 +8,7 @@ const uri = process.env.MONGO_URI async function connectDB() { try { await mongoose.connect(uri, { - serverSelectionTimeoutMS: 5000, // ⏳ Wait max 5 sec for MongoDB + serverSelectionTimeoutMS: 5000, }); console.log('Connected to MongoDB using Mongoose'); diff --git a/backend/src/server/app.js b/backend/src/server/app.js index a22cd289..30d8e009 100644 --- a/backend/src/server/app.js +++ b/backend/src/server/app.js @@ -6,31 +6,18 @@ import { dirname, join } from 'path'; import apiKeyPreHandler from './middlewares/apiKey.preHandler.js'; import connectDB from '../data/models/db.js'; import seedDb from './seed.js'; +import { logger, initLogger, streamToElastic } from './utils/logging/logger.js'; import './background-processing/bullmq.js' - const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -const envToLogger = { - DEVELOPMENT: { - transport: { - targets: [ - { - level: 'info', - target: 'pino-pretty', - options: {} - } - ], - }, - }, - production: true, - test: false, -} - -const app = Fastify({ logger: envToLogger[process.env.NODE_ENV] ?? true }); +var childLogger = logger.child({source:'node-api'}); + +const app = Fastify({ loggerInstance: childLogger }); +await initLogger(); await connectDB(); await seedDb(); @@ -65,5 +52,15 @@ app.setErrorHandler((error, request, reply) => { }); +// Capture errors like unable to connect Elasticsearch instance. + streamToElastic.on('error', (error) => { + console.error('Elasticsearch client error:', error); +}) +// Capture errors returned from Elasticsearch, "it will be called every time a document can't be indexed". +streamToElastic.on('insertError', (error) => { + console.error('Elasticsearch server error:', error); +}) + + export default app; diff --git a/backend/src/server/background-processing/jobs/callback-api-handler-job.js b/backend/src/server/background-processing/jobs/callback-api-handler-job.js index 6f2b9626..182d3134 100644 --- a/backend/src/server/background-processing/jobs/callback-api-handler-job.js +++ b/backend/src/server/background-processing/jobs/callback-api-handler-job.js @@ -1,11 +1,13 @@ import Source from "../../../data/models/Source.js"; import axios from "axios"; import axiosRetry from "axios-retry"; -import logger from "../../utils/logger.js"; +import {logger as parentLogger} from '../../utils/logging/logger.js'; import Log from "../../../data/models/Log.js"; import {checkSourceCredentialsExpired} from '../../utils/credentials.service.js' import { credentialsExpirationEmail } from "../../utils/email.js"; +const logger = parentLogger.child({source:'background-processing'}); + axiosRetry(axios, { retries: 3, retryCondition: (error) => { @@ -138,7 +140,7 @@ export const handleLogFetch = async (job) => { const generateLogs = () => { let logs = []; - for (let i = 0; i < 5000; i++) { + for (let i = 0; i < 500; i++) { const log = { id: `log-id-${i + 1}`, timestamp: new Date().toISOString(), // Current timestamp diff --git a/backend/src/server/background-processing/jobs/credential-expiration-job.js b/backend/src/server/background-processing/jobs/credential-expiration-job.js index 25f1761b..cec9d808 100644 --- a/backend/src/server/background-processing/jobs/credential-expiration-job.js +++ b/backend/src/server/background-processing/jobs/credential-expiration-job.js @@ -1,10 +1,12 @@ import Source from "../../../data/models/Source.js" import { Worker } from "bullmq" import { redisConnection } from '../config/redis-connection.js'; -import logger from '../../utils/logger.js'; +import {logger as parentLogger} from '../../utils/logging/logger.js'; import {checkSourceCredentialsExpired} from '../../utils/credentials.service.js' import { credentialsExpirationEmail } from "../../utils/email.js"; +const logger = parentLogger.child({source:'background-processing'}); + export const CREDENTIAL_EXPIRATION_JOB_NAME = "credentialsExpirationJob"; // eslint-disable-next-line no-unused-vars export const credentialsExpirationJob = async (job) => { diff --git a/backend/src/server/background-processing/jobs/log-fetching-scheduler-job.js b/backend/src/server/background-processing/jobs/log-fetching-scheduler-job.js index b5fdc2f4..56f5072e 100644 --- a/backend/src/server/background-processing/jobs/log-fetching-scheduler-job.js +++ b/backend/src/server/background-processing/jobs/log-fetching-scheduler-job.js @@ -1,7 +1,9 @@ import Source from "../../../data/models/Source.js" -import logger from '../../utils/logger.js'; +import {logger as parentLogger} from '../../utils/logging/logger.js'; import { logFetchingQueue } from "../queues/log-fetching-queue.js"; +const logger = parentLogger.child({source:'background-processing'}); + // eslint-disable-next-line no-unused-vars export const scheduleLogFetchingJobs = async (job) => { try{ diff --git a/backend/src/server/utils/credentials.service.js b/backend/src/server/utils/credentials.service.js index 48b34097..e3f3dd6c 100644 --- a/backend/src/server/utils/credentials.service.js +++ b/backend/src/server/utils/credentials.service.js @@ -1,5 +1,5 @@ import { google } from 'googleapis'; -import logger from './logger.js' +import {logger} from './logging/logger.js' export const checkSourceCredentialsExpired = async (source) => { try { diff --git a/backend/src/server/utils/email.js b/backend/src/server/utils/email.js index 8f738bd9..739de084 100644 --- a/backend/src/server/utils/email.js +++ b/backend/src/server/utils/email.js @@ -1,5 +1,5 @@ import { createTransport } from "nodemailer"; -import logger from "./logger.js"; +import {logger} from './logging/logger.js' export const credentialsExpirationEmail = async (id, email) => { let transporter = createTransport({ diff --git a/backend/src/server/utils/logger.js b/backend/src/server/utils/logger.js deleted file mode 100644 index ee16c8f7..00000000 --- a/backend/src/server/utils/logger.js +++ /dev/null @@ -1,17 +0,0 @@ -import pino from 'pino'; - - const logger = pino({ - transport: { - targets: [ - { - level: 'info', - target: 'pino-pretty', - options: {} - }, - ], - }, - }); - - - -export default logger; \ No newline at end of file diff --git a/backend/src/server/utils/logging/logger.js b/backend/src/server/utils/logging/logger.js new file mode 100644 index 00000000..55dbf73a --- /dev/null +++ b/backend/src/server/utils/logging/logger.js @@ -0,0 +1,73 @@ +import pino from 'pino'; +import ecsFormat from '@elastic/ecs-pino-format'; +import { Client } from '@elastic/elasticsearch'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const LOG_LEVEL = process.env.LOG_LEVEL || 'info'; + +// Initialize Elasticsearch Client +const esClient = new Client({ node: process.env.ELASTIC_SEARCH_NODE }); + +// Define Indexes +const INDEXES = { + application: 'app-logs' + }; + + async function ensureIndexes() { + for (const index of Object.values(INDEXES)) { + try { + const { body: exists } = await esClient.indices.exists({ index }); + + if (!exists) { + console.log(`Creating index: ${index}`); + await esClient.indices.create({ + index, + body: { + settings: { number_of_shards: 1, number_of_replicas: 1 }, + }, + }); + } + } catch (error) { + console.error(`Error checking/creating index "${index}":`, error.message); + } + } + } + + +console.log(process.env.ELASTIC_SEARCH_NODE) + +// Create Elasticsearch Transport +const streamToElastic = pino.transport({ + target: 'pino-elasticsearch', + options: { + node: process.env.ELASTIC_SEARCH_NODE, + esVersion: 7, + flushBytes: 1000, + index: INDEXES.application, + }, + }) + + // Console transport + const consoleTransport = pino.transport({ + target: 'pino-pretty', + options: { colorize: true }, + }); + + // global logger + const logger = pino( + { level: LOG_LEVEL }, + pino.multistream([ + { stream: consoleTransport }, + ...(streamToElastic ? [{ stream: streamToElastic, formatter: ecsFormat }] : []), + ]) + ); + + + async function initLogger() { + await ensureIndexes(); + console.log('Indexes ensured and logger initialized.'); + } + +export { logger, initLogger, streamToElastic }; From 54cffe029979cc0e5caff5589fbccfed52657934 Mon Sep 17 00:00:00 2001 From: Bleron213 Date: Fri, 14 Mar 2025 19:32:47 +0100 Subject: [PATCH 20/22] Update README.md --- README.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b09ae5a0..e91c6c9a 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ NODE_DEBUG=bull NODE_ENV=DEVELOPMENT CALLBACK_API_HOOK=http://localhost:8080/Hooks/SendLog DOCKER=0 +ELASTIC_SEARCH_NODE=http://localhost:9200 + ``` 4. Run these Docker commands: @@ -48,7 +50,10 @@ DOCKER=0 docker run --name mongodb -d -p 27017:27017 mongo docker run --name redis-server -p 6379:6379 -d redis docker run -d -p 8080:8080 --name callbackapi-container -e ASPNETCORE_ENVIRONMENT=Development bleronqorri/callbackapi:latest +docker network create backend-challenge-network +docker run -d --name elasticsearch --network backend-challenge-network -e "discovery.type=single-node" -e "xpack.security.enabled=false" -p 9200:9200 docker.elastic.co/elasticsearch/elasticsearch:8.3.3 +docker run -d --name kibana --network backend-challenge-network -p 5601:5601 -e ELASTICSEARCH_HOSTS="http://elasticsearch:9200" -e XPACK_SECURITY_ENABLED=false docker.elastic.co/kibana/kibana:8.3.3 ``` 5. Install dependencies and start the backend: @@ -98,7 +103,7 @@ The backend challenge should now be up and running. You can inspect the console --- -## Seein everything in action +## Seeing everything in action #### 1. Swagger documentation for endpoints @@ -130,12 +135,27 @@ We can see the following info: Here we can see logs being processed. Due to the aggressive rate limiter in the callback API, retries will be quite common. +#### 5. Elastic and Kibana + +If we navigate to KIbana -> Left Hamburger Menu -> Discover, we can see the following screen + +![image](https://github.com/user-attachments/assets/5a3adbca-beea-4e7c-b70b-51579e86ba6c) + +This means that Elastic search is accepting logs. To view them, we can create a new view + +![image](https://github.com/user-attachments/assets/28242240-8ef9-4381-973b-23f1900ee641) + +And we will be able to see application logs flowing in from Node.js app. Through the use of child loggers, we can differentiate between background processes and fastify api logs. + +Note that we're not restricted to application logs. We can create indexes for other things such as business events, products - anything. For now, we can see application logs flowing in seamlessly. + --- ## Notes - In a production environment, we would never expose API keys or encryption keys like this. For demo purposes, this is fine. - callbackapi-container might have issues on mac. If it doesn't work, please use the following command (if locally) +- For demo purposes, security has been disabled in Elastic & Kibana. ```bash docker run -d --platform linux/amd64 -p 8080:8080 --name callbackapi-container -e ASPNETCORE_ENVIRONMENT=Development bleronqorri/callbackapi:latest From ff6d108f700c79e5d7bb5c2afdd02cd08f1d7c17 Mon Sep 17 00:00:00 2001 From: Bleron213 Date: Fri, 14 Mar 2025 19:34:11 +0100 Subject: [PATCH 21/22] Update README.md --- README.md | 70 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index e91c6c9a..69ef5fc8 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,42 @@ ## How To Use -### 1. Non-Dockerized Solution +### 1. Dockerized Solution + +#### Clone the Repository + +```bash +# Clone this repository +$ git clone https://github.com/Bleron213/backend-challenge +``` + +#### Setting Up the Local Environment + +1. Open the folder where the solution was cloned. +2. Open a terminal and move to the `backend` folder. +3. Inside the `backend` folder, create a file named `.env` and place these environment variables inside: + +```dotenv +API_KEY=bBJ4Gig5CEVzTWM8l2nVCzX8Ht7IohuAFgsKK1puNmGU4FZormELBoRtjPySs4bAX6st4VOO2Vx8CSxoiQQuzWrrhEWlw2mwF17Boo5hun9Wo0RZZGhgsoK7uXSBD8AR +ENCRYPTION_KEY=0d932b4a920075ca6bd78fb589b9815d878b1bd06fbf1f7477b69102e8967908 +NODE_ENV=DEVELOPMENT +DOCKER=1 +``` + +4. Start the Docker containers: + +```bash +# Start the containers with Docker Compose +$ docker-compose up +``` + +The backend challenge should now be up and running. You can inspect the console to see logs. Alternatively, you can connect to MongoDB to view data inside the `sourcedb` and connect to Redis Insights to view the job scheduling inside Redis. + +--- + +### 2. Non-Dockerized Solution + +If for any reason you want to start the solution without docker, here's how: #### Clone the Repository @@ -70,39 +105,6 @@ The backend challenge should now be up and running. You can inspect the console --- -### 2. Dockerized Solution - -#### Clone the Repository - -```bash -# Clone this repository -$ git clone https://github.com/Bleron213/backend-challenge -``` - -#### Setting Up the Local Environment - -1. Open the folder where the solution was cloned. -2. Open a terminal and move to the `backend` folder. -3. Inside the `backend` folder, create a file named `.env` and place these environment variables inside: - -```dotenv -API_KEY=bBJ4Gig5CEVzTWM8l2nVCzX8Ht7IohuAFgsKK1puNmGU4FZormELBoRtjPySs4bAX6st4VOO2Vx8CSxoiQQuzWrrhEWlw2mwF17Boo5hun9Wo0RZZGhgsoK7uXSBD8AR -ENCRYPTION_KEY=0d932b4a920075ca6bd78fb589b9815d878b1bd06fbf1f7477b69102e8967908 -NODE_ENV=DEVELOPMENT -DOCKER=1 -``` - -4. Start the Docker containers: - -```bash -# Start the containers with Docker Compose -$ docker-compose up -``` - -The backend challenge should now be up and running. You can inspect the console to see logs. Alternatively, you can connect to MongoDB to view data inside the `sourcedb` and connect to Redis Insights to view the job scheduling inside Redis. - ---- - ## Seeing everything in action #### 1. Swagger documentation for endpoints From d311c135ec780355b349e66a12461e984e3d6ed7 Mon Sep 17 00:00:00 2001 From: Bleron213 Date: Fri, 14 Mar 2025 19:34:30 +0100 Subject: [PATCH 22/22] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 69ef5fc8..28b9933d 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ * Node.js
* MongoDB
* Redis
+* Elasticsearch and Kibana
---