From cc770037755bd8688026cf63cffa1b92b3bd27a0 Mon Sep 17 00:00:00 2001 From: Angel Crujera Date: Thu, 25 Sep 2025 16:05:57 +0200 Subject: [PATCH] Node.js v24 Upgrade & Linter --- .github/workflows/lint.yml | 28 + .nvmrc | 1 + eslint.config.js | 66 + package-lock.json | 6493 ++++++-------------- package.json | 15 +- scripts/otto-upload.js | 1579 ++--- src/config/database.js | 160 +- src/config/logger.js | 92 +- src/config/publicContexts.js | 24 +- src/controllers/ChunkedUploadController.js | 785 +-- src/controllers/FileController.js | 1251 ++-- src/controllers/HomeController.js | 78 +- src/controllers/UploadController.js | 379 +- src/middleware/auth.js | 336 +- src/middleware/chunkUpload.js | 304 +- src/middleware/errorHandler.js | 140 +- src/middleware/rateLimiter.js | 68 +- src/middleware/requestLogger.js | 52 +- src/middleware/upload.js | 388 +- src/models/File.js | 282 +- src/routes/chunkedUpload.js | 72 +- src/routes/files.js | 23 +- src/routes/health.js | 211 +- src/routes/public.js | 20 +- src/routes/upload.js | 34 +- src/scripts/migrate.js | 293 +- src/server.js | 207 +- src/services/ChunkedUploadService.js | 774 +-- src/services/FileService.js | 1014 +-- src/services/TokenService.js | 188 +- start.js | 100 +- 31 files changed, 6393 insertions(+), 9064 deletions(-) create mode 100644 .github/workflows/lint.yml create mode 100644 .nvmrc create mode 100644 eslint.config.js diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..d0b2973 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,28 @@ +name: Lint + +on: + push: + branches: ['**'] + pull_request: + branches: ['**'] + workflow_dispatch: + +jobs: + eslint: + name: Run ESLint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '24' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run ESLint + run: npm run lint -- --max-warnings=0 diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..cabf43b --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..d44e22b --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,66 @@ +import { defineConfig, globalIgnores } from 'eslint/config' +import globals from 'globals' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import js from '@eslint/js' +import { FlatCompat } from '@eslint/eslintrc' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}) + +export default defineConfig([ + globalIgnores([ + '**/dist', + '**/node_modules/**', + '**/build/**', + './eslint.config.mjs', + ]), + { + extends: compat.extends('eslint:recommended'), + + languageOptions: { + globals: { + ...globals.node, + }, + + ecmaVersion: 2022, + sourceType: 'module', + }, + + rules: { + indent: [ + 'error', + 4, + { + SwitchCase: 1, + }, + ], + + 'linebreak-style': ['error', 'windows'], + quotes: ['error', 'single'], + semi: ['error', 'never'], + 'no-var': ['error'], + 'no-console': [0], + 'no-control-regex': [0], + + 'no-unused-vars': [ + 'error', + { + vars: 'all', + args: 'none', + ignoreRestSiblings: false, + argsIgnorePattern: 'reject', + }, + ], + + 'no-async-promise-executor': [0], + 'no-undef': 0, + }, + }, + {}, +]) diff --git a/package-lock.json b/package-lock.json index 6cf87be..4ea4190 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "otto", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "otto", - "version": "1.0.0", + "version": "1.1.0", "license": "AGPL-3.0", "dependencies": { "bcrypt": "^5.1.1", @@ -31,88 +31,94 @@ "winston": "^3.11.0" }, "devDependencies": { - "jest": "^29.7.0", - "nodemon": "^3.0.2", - "supertest": "^6.3.3" + "eslint": "^9.36.0", + "nodemon": "^3.0.2" }, "engines": { - "node": ">=22.0.0" + "node": ">=24.0.0" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", "engines": { - "node": ">=6.0.0" + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" } }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": ">=6.9.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@babel/compat-data": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz", - "integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==", + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">=6.9.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@babel/core": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", - "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.4", - "@babel/parser": "^7.27.4", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.27.4", - "@babel/types": "^7.27.3", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" }, "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@babel/core/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "node_modules/@eslint/config-array/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -127,3643 +133,1396 @@ } } }, - "node_modules/@babel/core/node_modules/ms": { + "node_modules/@eslint/config-array/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/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", - "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "node_modules/@eslint/config-helpers": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.27.5", - "@babel/types": "^7.27.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, + "license": "Apache-2.0", "engines": { - "node": ">=6.9.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" + "@types/json-schema": "^7.0.15" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" }, "engines": { - "node": ">=6.9.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "ms": "^2.1.3" }, "engines": { - "node": ">=6.9.0" + "node": ">=6.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "node_modules/@eslint/eslintrc/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", - "engines": { - "node": ">=6.9.0" - } + "license": "MIT" }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "node_modules/@eslint/js": { + "version": "9.36.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.36.0.tgz", + "integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==", "dev": true, "license": "MIT", "engines": { - "node": ">=6.9.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">=6.9.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, "engines": { - "node": ">=6.9.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@babel/helpers": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", - "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", - "dev": true, - "license": "MIT", + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.27.6" - }, + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=6.9.0" + "node": ">=18.18.0" } }, - "node_modules/@babel/parser": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", - "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@babel/types": "^7.27.3" - }, - "bin": { - "parser": "bin/babel-parser.js" + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" }, "engines": { - "node": ">=6.0.0" + "node": ">=18.18.0" } }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "license": "MIT", + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" } }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, + "node_modules/@mapbox/node-pre-gyp/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": { - "@babel/helper-plugin-utils": "^7.14.5" + "whatwg-url": "^5.0.0" }, "engines": { - "node": ">=6.9.0" + "node": "4.x || >=6.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" } }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "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==", "dev": true, + "license": "MIT" + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "event-target-shim": "^5.0.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=6.5" } }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, + "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", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "mime-types": "~2.1.34", + "negotiator": "0.6.3" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">= 0.6" } }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, + "node_modules/accepts/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", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">= 0.6" } }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "bin": { + "acorn": "bin/acorn" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=0.4.0" } }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, + "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", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "debug": "4" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">= 6.0.0" } }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "ms": "^2.1.3" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "engines": { + "node": ">=6.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } + "node_modules/agent-base/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/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, + "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", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=8" } }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=6.9.0" + "node": ">=8" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "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": "MIT", + "license": "ISC", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">= 8" } }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" }, - "node_modules/@babel/traverse": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", - "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", - "dev": true, - "license": "MIT", + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", - "@babel/parser": "^7.27.4", - "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3", - "debug": "^4.3.1", - "globals": "^11.1.0" + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=10" } }, - "node_modules/@babel/traverse/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "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==", "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" + "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" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "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/b4a": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.2.tgz", + "integrity": "sha512-DyUOdz+E8R6+sruDpQNOaV0y/dBbV6X/8ZkxrDcR0Ifc3BgKlpgG0VAtfOozA0eMtJO5GGe9FsZhueLs00pTww==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" }, "peerDependenciesMeta": { - "supports-color": { + "react-native-b4a": { "optional": true } } }, - "node_modules/@babel/traverse/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, + "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/@babel/types": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", - "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", - "dev": true, - "license": "MIT", + "node_modules/bare-events": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.7.0.tgz", + "integrity": "sha512-b3N5eTW1g7vXkw+0CXh/HazGTcO5KYuu/RCNaJbDMPI6LHDi+7qe8EmxKUVe1sUbY2KZOVZFyj62x0OEz9qyAA==", + "license": "Apache-2.0" + }, + "node_modules/bare-fs": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.4.4.tgz", + "integrity": "sha512-Q8yxM1eLhJfuM7KXVP3zjhBvtMJCYRByoTT+wHXjpdMELv0xICFJX+1w4c7csa+WZEOsq4ItJ4RGwvzid6m/dw==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" }, "engines": { - "node": ">=6.9.0" + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } } }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@colors/colors": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", - "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", - "license": "MIT", + "node_modules/bare-os": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", + "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", + "license": "Apache-2.0", + "optional": true, "engines": { - "node": ">=0.1.90" + "bare": ">=1.14.0" } }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", - "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", - "license": "MIT", + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "colorspace": "1.1.x", - "enabled": "2.0.x", - "kuler": "^2.0.0" + "bare-os": "^3.0.1" } }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", - "license": "BSD-3-Clause" + "node_modules/bare-stream": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", + "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "license": "BSD-3-Clause", + "node_modules/bare-url": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.2.2.tgz", + "integrity": "sha512-g+ueNGKkrjMazDG3elZO1pNs3HY5+mMmOet1jtKyhOaCnkLzitxf26z7hoAEkDNgdNmnc1KIlt/dw6Po6xZMpA==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@hapi/hoek": "^9.0.0" + "bare-path": "^3.0.0" } }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "license": "ISC", + "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/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">= 10.0.0" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "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/@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "dev": true, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" } }, - "node_modules/@jest/console/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "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", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "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": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/@jest/core": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", - "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "dev": true, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "license": "MIT", "dependencies": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/@jest/core/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "fill-range": "^7.1.1" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=8" } }, - "node_modules/@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "dev": true, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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", "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" } }, - "node_modules/@jest/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "dev": true, - "license": "MIT", + "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/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" + "streamsearch": "^1.1.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=10.16.0" } }, - "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "license": "MIT", - "dependencies": { - "jest-get-type": "^29.6.3" - }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 0.8" } }, - "node_modules/@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "dev": true, + "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": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 0.4" } }, - "node_modules/@jest/globals": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "dev": true, + "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", "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@jest/reporters": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", - "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } + "engines": { + "node": ">=6" } }, - "node_modules/@jest/reporters/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "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": { - "@sinclair/typebox": "^0.27.8" + "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": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, - "node_modules/@jest/source-map": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", - "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=10" } }, - "node_modules/@jest/test-result": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "dev": true, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", "license": "MIT", "dependencies": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" + "color-convert": "^2.0.1", + "color-string": "^1.9.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=12.5.0" } }, - "node_modules/@jest/test-sequencer": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", - "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "dev": true, + "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": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "slash": "^3.0.0" + "color-name": "~1.1.4" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=7.0.0" } }, - "node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, + "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/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", "license": "MIT", "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" } }, - "node_modules/@jest/transform/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "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/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" } }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", "license": "MIT", "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "color": "^3.1.3", + "text-hex": "1.0.x" } }, - "node_modules/@jest/types/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "node_modules/colorspace/node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", "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" + "color-convert": "^1.9.3", + "color-string": "^1.6.0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "dev": true, + "node_modules/colorspace/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" + "color-name": "1.1.3" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, + "node_modules/colorspace/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "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": ">=6.0.0" + "node": ">= 0.8" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, + "node_modules/commander": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz", + "integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==", "license": "MIT", "engines": { - "node": ">=6.0.0" + "node": ">=20" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "license": "BSD-3-Clause", - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" + "mime-db": ">= 1.43.0 < 2" }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" + "engines": { + "node": ">= 0.6" } }, - "node_modules/@mapbox/node-pre-gyp/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==", + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "license": "MIT", "dependencies": { - "whatwg-url": "^5.0.0" + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" }, "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "node": ">= 0.8.0" } }, - "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } + "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/@paralleldrive/cuid2": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", - "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", - "dev": true, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], "license": "MIT", "dependencies": { - "@noble/hashes": "^1.1.5" + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" } }, - "node_modules/@sideway/address": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", - "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", - "license": "BSD-3-Clause", + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "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": { - "@hapi/hoek": "^9.0.0" + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" } }, - "node_modules/@sideway/formula": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", - "license": "BSD-3-Clause" - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" + "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", + "engines": { + "node": ">= 0.6" } }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.0" + "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", + "engines": { + "node": ">= 0.6" } }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "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" }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" } }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.0.0" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" } }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" + "engines": { + "node": ">= 12" } }, - "node_modules/@types/babel__traverse": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", - "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", - "dev": true, + "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", "dependencies": { - "@babel/types": "^7.20.7" + "ms": "2.0.0" } }, - "node_modules/@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "dev": true, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "license": "MIT", "dependencies": { - "@types/node": "*" + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" + "engines": { + "node": ">=4.0.0" } }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } + "license": "MIT" }, - "node_modules/@types/node": { - "version": "22.15.29", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz", - "integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==", - "dev": true, + "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", - "dependencies": { - "undici-types": "~6.21.0" + "engines": { + "node": ">=0.4.0" } }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/triple-beam": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", - "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", - "license": "MIT" - }, - "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "license": "ISC" - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "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", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/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", - "engines": { - "node": ">= 0.6" - } - }, - "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", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/agent-base/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/agent-base/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/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "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/append-field": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", - "license": "MIT" - }, - "node_modules/aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", - "license": "ISC" - }, - "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "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" - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true, - "license": "MIT" - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT" - }, - "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/b4a": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", - "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", - "license": "Apache-2.0" - }, - "node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/babel-jest/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "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/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", - "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "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/bare-events": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", - "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", - "license": "Apache-2.0", - "optional": true - }, - "node_modules/bare-fs": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.5.tgz", - "integrity": "sha512-1zccWBMypln0jEE05LzZt+V/8y8AQsQQqxtklqaIyg5nu6OAYFhZxPXinJTSG+kU5qyNmeLgcn9AW7eHiCHVLA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4" - }, - "engines": { - "bare": ">=1.16.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-os": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", - "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "bare": ">=1.14.0" - } - }, - "node_modules/bare-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", - "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-os": "^3.0.1" - } - }, - "node_modules/bare-stream": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", - "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "streamx": "^2.21.0" - }, - "peerDependencies": { - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "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/bcrypt": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", - "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.11", - "node-addon-api": "^5.0.0" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "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/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "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", - "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/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "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/browserslist": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", - "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001718", - "electron-to-chromium": "^1.5.160", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "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", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "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/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" - }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.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", - "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", - "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/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001721", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz", - "integrity": "sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "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/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", - "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.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/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "license": "ISC", - "bin": { - "color-support": "bin.js" - } - }, - "node_modules/colorspace": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", - "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", - "license": "MIT", - "dependencies": { - "color": "^3.1.3", - "text-hex": "1.0.x" - } - }, - "node_modules/colorspace/node_modules/color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", - "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.3", - "color-string": "^1.6.0" - } - }, - "node_modules/colorspace/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/colorspace/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "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": "14.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", - "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/component-emitter": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", - "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "license": "MIT", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "compressible": "~2.0.18", - "debug": "2.6.9", - "negotiator": "~0.6.4", - "on-headers": "~1.1.0", - "safe-buffer": "5.2.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "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/concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "engines": [ - "node >= 6.0" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "license": "ISC" - }, - "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", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "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", - "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" - }, - "node_modules/cookiejar": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", - "dev": true, - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/create-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", - "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "prompts": "^2.0.1" - }, - "bin": { - "create-jest": "bin/create-jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/create-jest/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "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/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "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", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/dedent": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", - "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.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/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "license": "MIT" - }, - "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/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/dezalgo": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", - "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", - "dev": true, - "license": "ISC", - "dependencies": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/dotenv": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", - "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/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", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.165", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.165.tgz", - "integrity": "sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw==", - "dev": true, - "license": "ISC" - }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "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/enabled": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", - "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", - "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==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "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/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "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/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "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", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "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", - "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/express-rate-limit": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", - "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": "^4.11 || 5 || ^5.0.0-beta.1" - } - }, - "node_modules/express-validator": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz", - "integrity": "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==", - "license": "MIT", - "dependencies": { - "lodash": "^4.17.21", - "validator": "~13.12.0" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "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/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", - "license": "MIT" - }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, - "node_modules/file-type": { - "version": "18.7.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-18.7.0.tgz", - "integrity": "sha512-ihHtXRzXEziMrQ56VSgU7wkxh55iNchFkosu7Y9/S+tXHdKyrGjVK0ujbqNnsxzea+78MaLhN6PGmfYSAv1ACw==", - "license": "MIT", - "dependencies": { - "readable-web-to-node-stream": "^3.0.2", - "strtok3": "^7.0.0", - "token-types": "^5.0.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, - "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", - "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-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", - "license": "MIT" - }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/formidable": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.5.tgz", - "integrity": "sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@paralleldrive/cuid2": "^2.2.2", - "dezalgo": "^1.0.4", - "once": "^1.4.0", - "qs": "^6.11.0" - }, - "funding": { - "url": "https://ko-fi.com/tunnckoCore/commissions" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "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", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs-minipass/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, - "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/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "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-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "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/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "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.1.1", - "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/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "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/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "license": "ISC" - }, - "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/helmet": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", - "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "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/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", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "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/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "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", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "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": "BSD-3-Clause" - }, - "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/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, + "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", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz", + "integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==", + "license": "Apache-2.0", "engines": { "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://dotenvx.com" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, + "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.8.19" + "node": ">= 0.4" } }, - "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", + "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": { - "once": "^1.3.0", - "wrappy": "1" + "safe-buffer": "^5.0.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/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" + "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" }, - "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", - "engines": { - "node": ">= 0.10" - } + "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/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", "license": "MIT" }, - "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, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, "engines": { - "node": ">=8" + "node": ">= 0.8" } }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "license": "MIT", "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "once": "^1.4.0" } }, - "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, + "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.10.0" + "node": ">= 0.4" } }, - "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==", + "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": ">=8" + "node": ">= 0.4" } }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, + "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": ">=6" + "node": ">= 0.4" } }, - "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, + "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": { - "is-extglob": "^2.1.1" + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, - "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/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/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "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==", - "dev": true, - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "node_modules/eslint": { + "version": "9.36.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz", + "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.36.0", + "@eslint/plugin-kit": "^0.3.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" }, "engines": { - "node": ">=10" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, - "license": "BSD-3-Clause", + "license": "BSD-2-Clause", "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">=10" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/istanbul-lib-report/node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, + "license": "Apache-2.0", "engines": { - "node": ">=10" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/eslint" } }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/istanbul-lib-source-maps/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "node_modules/eslint/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -3778,848 +1537,903 @@ } } }, - "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/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/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, - "license": "BSD-3-Clause", + "license": "BSD-2-Clause", "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" }, "engines": { - "node": ">=8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "@jest/core": "^29.7.0", - "@jest/types": "^29.6.3", - "import-local": "^3.0.2", - "jest-cli": "^29.7.0" - }, - "bin": { - "jest": "bin/jest.js" + "estraverse": "^5.1.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } + "node": ">=0.10" } }, - "node_modules/jest-changed-files": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", - "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "execa": "^5.0.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0" + "estraverse": "^5.2.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" } }, - "node_modules/jest-circus": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", - "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, + "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", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^1.0.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.7.0", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0", - "pretty-format": "^29.7.0", - "pure-rand": "^6.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "bare-events": "^2.7.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=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", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "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": ">=10" + "node": ">= 0.10.0" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/jest-cli": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "dev": true, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", "license": "MIT", - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "create-jest": "^29.7.0", - "exit": "^0.1.2", - "import-local": "^3.0.2", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "yargs": "^17.3.1" - }, - "bin": { - "jest": "bin/jest.js" - }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" }, "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + "express": ">= 4.11" + } + }, + "node_modules/express-validator": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz", + "integrity": "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "validator": "~13.12.0" }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } + "engines": { + "node": ">= 8.0.0" } }, - "node_modules/jest-cli/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, + "license": "MIT" + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": "^12.20 || >= 14.13" } }, - "node_modules/jest-config": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", - "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-jest": "^29.7.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "ts-node": { - "optional": true - } + "node": ">=16.0.0" } }, - "node_modules/jest-config/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "node_modules/file-type": { + "version": "18.7.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-18.7.0.tgz", + "integrity": "sha512-ihHtXRzXEziMrQ56VSgU7wkxh55iNchFkosu7Y9/S+tXHdKyrGjVK0ujbqNnsxzea+78MaLhN6PGmfYSAv1ACw==", "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "readable-web-to-node-stream": "^3.0.2", + "strtok3": "^7.0.0", + "token-types": "^5.0.1" }, "engines": { - "node": ">=10" + "node": ">=14.16" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, - "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "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": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" + "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", + "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": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 0.8" } }, - "node_modules/jest-diff/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jest-docblock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", - "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { - "detect-newline": "^3.0.0" + "flatted": "^3.2.9", + "keyv": "^4.5.4" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=16" } }, - "node_modules/jest-each": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", - "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, + "license": "ISC" + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "jest-util": "^29.7.0", - "pretty-format": "^29.7.0" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 6" } }, - "node_modules/jest-each/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "fetch-blob": "^3.1.2" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=12.20.0" } }, - "node_modules/jest-environment-node": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", - "dev": true, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 0.6" } }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "license": "MIT", "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 0.6" } }, - "node_modules/jest-haste-map": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "dev": true, - "license": "MIT", + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", "dependencies": { - "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" + "minipass": "^3.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" + "node": ">= 8" } }, - "node_modules/jest-leak-detector": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", - "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "dev": true, - "license": "MIT", + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", "dependencies": { - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" + "yallist": "^4.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=8" } }, - "node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "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/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=10" } }, - "node_modules/jest-matcher-utils/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "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": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "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": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, + "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": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 0.4" } }, - "node_modules/jest-message-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=10" + "node": "*" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jest-mock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "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": "MIT", + "license": "ISC", "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-util": "^29.7.0" + "is-glob": "^4.0.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 6" } }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=18" }, - "peerDependencies": { - "jest-resolve": "*" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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" }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "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==", "dev": true, "license": "MIT", "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=8" } }, - "node_modules/jest-resolve": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", - "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "dev": true, + "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": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" + "has-symbols": "^1.0.3" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jest-resolve-dependencies": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", - "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "dev": true, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "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": { - "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.7.0" + "function-bind": "^1.1.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 0.4" } }, - "node_modules/jest-resolve/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "node_modules/helmet": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", + "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", "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": ">=16.0.0" } }, - "node_modules/jest-runner": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", - "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/environment": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-leak-detector": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-resolve": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-util": "^29.7.0", - "jest-watcher": "^29.7.0", - "jest-worker": "^29.7.0", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runner/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "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": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">= 0.8" } }, - "node_modules/jest-runtime": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", - "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "dev": true, + "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", "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/globals": "^29.7.0", - "@jest/source-map": "^29.6.3", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" + "agent-base": "6", + "debug": "4" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 6" } }, - "node_modules/jest-runtime/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "ms": "^2.1.3" }, "engines": { - "node": ">=10" + "node": ">=6.0" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/jest-snapshot": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", - "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "dev": 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", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "license": "MIT", "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "natural-compare": "^1.4.0", - "pretty-format": "^29.7.0", - "semver": "^7.5.3" + "safer-buffer": ">= 2.1.2 < 3" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=0.10.0" } }, - "node_modules/jest-snapshot/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "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": ">= 4" } }, - "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "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": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } + "license": "ISC" }, - "node_modules/jest-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, "engines": { - "node": ">=10" + "node": ">=6" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jest-validate": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", - "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "leven": "^3.1.0", - "pretty-format": "^29.7.0" - }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=0.8.19" } }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, + "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/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "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", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.10" } }, - "node_modules/jest-validate/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "binary-extensions": "^2.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=8" } }, - "node_modules/jest-watcher": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "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", - "dependencies": { - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "jest-util": "^29.7.0", - "string-length": "^4.0.1" - }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=0.10.0" } }, - "node_modules/jest-watcher/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "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", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=8" } }, - "node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "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": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" + "is-extglob": "^2.1.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=0.10.0" } }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "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", - "dependencies": { - "has-flag": "^4.0.0" - }, "engines": { - "node": ">=10" + "node": ">=0.12.0" + } + }, + "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/chalk/supports-color?sponsor=1" + "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==", + "dev": true, + "license": "ISC" + }, "node_modules/joi": { "version": "17.13.3", "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", @@ -4633,59 +2447,39 @@ "@sideway/pinpoint": "^2.0.0" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } + "license": "MIT" }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } + "license": "MIT" }, "node_modules/jsonwebtoken": { "version": "9.0.2", @@ -4736,14 +2530,14 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6" + "dependencies": { + "json-buffer": "3.0.1" } }, "node_modules/kuler": { @@ -4752,34 +2546,34 @@ "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", "license": "MIT" }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, "engines": { - "node": ">=6" + "node": ">= 0.8.0" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { - "p-locate": "^4.1.0" + "p-locate": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/lodash": { @@ -4824,6 +2618,13 @@ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "license": "MIT" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -4853,16 +2654,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -4887,16 +2678,6 @@ "semver": "bin/semver.js" } }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tmpl": "1.0.5" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4924,13 +2705,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -4940,20 +2714,6 @@ "node": ">= 0.6" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -4996,16 +2756,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -5144,9 +2894,9 @@ } }, "node_modules/node-abi": { - "version": "3.75.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", - "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", + "version": "3.77.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.77.0.tgz", + "integrity": "sha512-DSmt0OEcLoK4i3NuscSbGjOf3bqiDEutejqENSplMSFA/gmB8mkED9G4pKWnPl7MDU4rSHebKPHeitpDfyH0cQ==", "license": "MIT", "dependencies": { "semver": "^7.3.5" @@ -5199,20 +2949,6 @@ "url": "https://opencollective.com/node-fetch" } }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT" - }, "node_modules/nodemon": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", @@ -5243,9 +2979,9 @@ } }, "node_modules/nodemon/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -5315,19 +3051,6 @@ "node": ">=0.10.0" } }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/npmlog": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", @@ -5401,20 +3124,22 @@ "fn.name": "1.x.x" } }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { - "mimic-fn": "^2.1.0" + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.8.0" } }, "node_modules/p-limit": { @@ -5434,61 +3159,32 @@ } }, "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { - "p-try": "^2.0.0" + "p-limit": "^3.0.2" }, "engines": { - "node": ">=6" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" + "callsites": "^3.0.0" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, "node_modules/parseurl": { @@ -5529,13 +3225,6 @@ "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, "node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", @@ -5556,22 +3245,22 @@ } }, "node_modules/pg": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.0.tgz", - "integrity": "sha512-7SKfdvP8CTNXjMUzfcVTaI+TDzBEeaUnVwiVGZQD1Hh33Kpev7liQba9uLd4CfN8r9mCVsD0JIpq03+Unpz+kg==", + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", "dependencies": { - "pg-connection-string": "^2.9.0", - "pg-pool": "^3.10.0", - "pg-protocol": "^1.10.0", + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "engines": { - "node": ">= 8.0.0" + "node": ">= 16.0.0" }, "optionalDependencies": { - "pg-cloudflare": "^1.2.5" + "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" @@ -5583,16 +3272,16 @@ } }, "node_modules/pg-cloudflare": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.5.tgz", - "integrity": "sha512-OOX22Vt0vOSRrdoUPKJ8Wi2OpE/o/h9T8X1s4qSkCedbNah9ei2W2765be8iMVxQUsvgT7zIAT2eIa9fs5+vtg==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", "license": "MIT", "optional": true }, "node_modules/pg-connection-string": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.0.tgz", - "integrity": "sha512-P2DEBKuvh5RClafLngkAuGe9OUlFV7ebu8w1kmaaOgPcpJd1RIFh7otETfI6hAR8YupOLFTY7nuvvIn7PLciUQ==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", "license": "MIT" }, "node_modules/pg-int8": { @@ -5605,18 +3294,18 @@ } }, "node_modules/pg-pool": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.0.tgz", - "integrity": "sha512-DzZ26On4sQ0KmqnO34muPcmKbhrjmyiO4lCCR0VwEd7MjmiKf5NTg/6+apUEu0NF7ESa37CGzFxH513CoUmWnA==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", "license": "MIT", "peerDependencies": { "pg": ">=8.0" } }, "node_modules/pg-protocol": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.0.tgz", - "integrity": "sha512-IpdytjudNuLv8nhlHs/UrVBhU0e78J0oIS/0AVdTbWxSOkFUVdsHC/NrorO6nXsQNDTT1kzDSOMJubBQviX18Q==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", "license": "MIT" }, "node_modules/pg-types": { @@ -5644,13 +3333,6 @@ "split2": "^4.1.0" } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -5664,29 +3346,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -5759,9 +3418,9 @@ "license": "ISC" }, "node_modules/prebuild-install/node_modules/tar-fs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", - "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "license": "MIT", "dependencies": { "chownr": "^1.1.1", @@ -5786,32 +3445,14 @@ "node": ">=6" } }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">= 0.8.0" } }, "node_modules/process": { @@ -5823,20 +3464,6 @@ "node": ">= 0.6.0" } }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -5858,31 +3485,24 @@ "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==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, - "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=6" + } }, "node_modules/qs": { "version": "6.13.0", @@ -5947,13 +3567,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -6005,109 +3618,46 @@ "license": "MIT", "dependencies": { "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/readable-web-to-node-stream/node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", - "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/readable-web-to-node-stream/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "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/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "ieee754": "^1.2.1" } }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, + "node_modules/readable-web-to-node-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", "license": "MIT", "dependencies": { - "resolve-from": "^5.0.0" + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" }, "engines": { - "node": ">=8" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "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" + "node": ">=8.10.0" } }, - "node_modules/resolve.exports": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", - "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "node": ">=4" } }, "node_modules/rimraf": { @@ -6415,18 +3965,18 @@ } }, "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", "license": "MIT", "dependencies": { "is-arrayish": "^0.3.1" } }, "node_modules/simple-swizzle/node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", "license": "MIT" }, "node_modules/simple-update-notifier": { @@ -6442,44 +3992,6 @@ "node": ">=10" } }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -6489,13 +4001,6 @@ "node": ">= 10.x" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -6505,19 +4010,6 @@ "node": "*" } }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -6536,45 +4028,23 @@ } }, "node_modules/streamx": { - "version": "2.22.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", - "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", "license": "MIT", "dependencies": { + "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" - }, - "optionalDependencies": { - "bare-events": "^2.2.0" } }, "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "license": "MIT", "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" + "safe-buffer": "~5.2.0" } }, "node_modules/string-width": { @@ -6603,26 +4073,6 @@ "node": ">=8" } }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -6653,81 +4103,6 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/superagent": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", - "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", - "deprecated": "Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net", - "dev": true, - "license": "MIT", - "dependencies": { - "component-emitter": "^1.3.0", - "cookiejar": "^2.1.4", - "debug": "^4.3.4", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.0", - "formidable": "^2.1.2", - "methods": "^1.1.2", - "mime": "2.6.0", - "qs": "^6.11.0", - "semver": "^7.3.8" - }, - "engines": { - "node": ">=6.4.0 <13 || >=14" - } - }, - "node_modules/superagent/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/superagent/node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/superagent/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/supertest": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.4.tgz", - "integrity": "sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "methods": "^1.1.2", - "superagent": "^8.1.2" - }, - "engines": { - "node": ">=6.4.0" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6741,19 +4116,6 @@ "node": ">=8" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -6772,9 +4134,9 @@ } }, "node_modules/tar-fs": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz", - "integrity": "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", "license": "MIT", "dependencies": { "pump": "^3.0.0", @@ -6814,21 +4176,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/text-decoder": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", @@ -6844,13 +4191,6 @@ "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", "license": "MIT" }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6927,27 +4267,17 @@ "node": "*" } }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" + "dependencies": { + "prelude-ls": "^1.2.1" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 0.8.0" } }, "node_modules/type-is": { @@ -6976,13 +4306,6 @@ "dev": true, "license": "MIT" }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -6992,35 +4315,14 @@ "node": ">= 0.8" } }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" + "punycode": "^2.1.0" } }, "node_modules/util-deprecate": { @@ -7051,21 +4353,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, "node_modules/validator": { "version": "13.12.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", @@ -7084,16 +4371,6 @@ "node": ">= 0.8" } }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "makeerror": "1.0.12" - } - }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -7180,22 +4457,14 @@ "node": ">= 12.0.0" } }, - "node_modules/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==", + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "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": ">=0.10.0" } }, "node_modules/wrappy": { @@ -7204,20 +4473,6 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, - "node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -7227,52 +4482,6 @@ "node": ">=0.4" } }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 1ed6662..aa36519 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,10 @@ { "name": "otto", - "version": "1.0.0", + "version": "1.1.0", "description": "The simple and efficient file server", "main": "src/server.js", - "type": "module", "scripts": { + "type": "module", + "scripts": { "start": "node start.js", "dev": "nodemon start.js", "test": "jest", @@ -11,7 +12,8 @@ "test:quick": "node test-otto.js quick", "test:chunked": "node test-chunked-upload.js", "test:routes": "node test-routes.js", - "seed": "node src/scripts/seed.js" + "seed": "node src/scripts/seed.js", + "lint": "eslint . --ext .js" }, "keywords": [ "file-upload", @@ -44,11 +46,10 @@ "winston": "^3.11.0" }, "devDependencies": { - "jest": "^29.7.0", - "nodemon": "^3.0.2", - "supertest": "^6.3.3" + "eslint": "^9.36.0", + "nodemon": "^3.0.2" }, "engines": { - "node": ">=22.0.0" + "node": ">=24.0.0" } } diff --git a/scripts/otto-upload.js b/scripts/otto-upload.js index 7d97b5b..76bf8a1 100644 --- a/scripts/otto-upload.js +++ b/scripts/otto-upload.js @@ -23,919 +23,924 @@ * node otto-upload.js --token SERVICE_TOKEN --context gallery --thumbnails *.jpg */ -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import FormData from 'form-data'; -import fetch from 'node-fetch'; -import { Command } from 'commander'; -import chalk from 'chalk'; -import { fileTypeFromFile } from 'file-type'; -import { spawn } from 'child_process'; -import { promisify } from 'util'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' +import FormData from 'form-data' +import fetch from 'node-fetch' +import { Command } from 'commander' +import chalk from 'chalk' +import { fileTypeFromFile } from 'file-type' +import { spawn } from 'child_process' +// eslint-disable-next-line no-unused-vars +import { promisify } from 'util' + +const __filename = fileURLToPath(import.meta.url) +// eslint-disable-next-line no-unused-vars +const __dirname = path.dirname(__filename) class OttoUploader { - constructor(options = {}) { - this.baseUrl = options.baseUrl || 'http://localhost:3000'; - this.token = options.token; - this.verbose = options.verbose || false; - } - - log(message, type = 'info') { - if (!this.verbose && type === 'debug') return; + constructor(options = {}) { + this.baseUrl = options.baseUrl || 'http://localhost:3000' + this.token = options.token + this.verbose = options.verbose || false + } + + log(message, type = 'info') { + if (!this.verbose && type === 'debug') return - const timestamp = new Date().toISOString(); - const prefix = type === 'error' ? chalk.red('ERROR') : - type === 'success' ? chalk.green('SUCCESS') : - type === 'debug' ? chalk.gray('DEBUG') : - chalk.blue('INFO'); + const timestamp = new Date().toISOString() + const prefix = type === 'error' ? chalk.red('ERROR') : + type === 'success' ? chalk.green('SUCCESS') : + type === 'debug' ? chalk.gray('DEBUG') : + chalk.blue('INFO') - console.log(`[${timestamp}] ${prefix}: ${message}`); - } - - formatBytes(bytes) { - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; - } - formatDuration(ms) { - if (ms < 0 || !isFinite(ms)) return 'calculating...'; + console.log(`[${timestamp}] ${prefix}: ${message}`) + } + + formatBytes(bytes) { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] + } + formatDuration(ms) { + if (ms < 0 || !isFinite(ms)) return 'calculating...' - const seconds = Math.floor((ms / 1000) % 60); - const minutes = Math.floor((ms / (1000 * 60)) % 60); - const hours = Math.floor((ms / (1000 * 60 * 60)) % 24); + const seconds = Math.floor((ms / 1000) % 60) + const minutes = Math.floor((ms / (1000 * 60)) % 60) + const hours = Math.floor((ms / (1000 * 60 * 60)) % 24) - if (hours > 0) { - return `${hours}h ${minutes}m ${seconds}s`; - } else if (minutes > 0) { - return `${minutes}m ${seconds}s`; - } else { - return `${seconds}s`; + if (hours > 0) { + return `${hours}h ${minutes}m ${seconds}s` + } else if (minutes > 0) { + return `${minutes}m ${seconds}s` + } else { + return `${seconds}s` + } } - } - /** + /** * Check if a file is a video format */ - isVideoFile(file) { + isVideoFile(file) { // Check MIME type first - if (file.mimeType && file.mimeType.startsWith('video/')) { - return true; - } + if (file.mimeType && file.mimeType.startsWith('video/')) { + return true + } - // Check file extension as fallback - const extension = path.extname(file.name).toLowerCase(); - const videoExtensions = [ - '.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.mkv', '.m4v', - '.3gp', '.ogv', '.f4v', '.asf', '.rm', '.rmvb', '.vob', '.ts', - '.mts', '.m2ts', '.divx', '.xvid', '.mpg', '.mpeg', '.m1v', '.m2v', - '.qt', '.amv', '.drc', '.gif', '.gifv', '.mng', '.qt', '.yuv', - '.roq', '.svi', '.viv', '.vp6', '.vp8', '.vp9', '.h264', '.h265', - '.hevc', '.prores', '.dnxhd', '.cineform' - ]; + // Check file extension as fallback + const extension = path.extname(file.name).toLowerCase() + const videoExtensions = [ + '.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.mkv', '.m4v', + '.3gp', '.ogv', '.f4v', '.asf', '.rm', '.rmvb', '.vob', '.ts', + '.mts', '.m2ts', '.divx', '.xvid', '.mpg', '.mpeg', '.m1v', '.m2v', + '.qt', '.amv', '.drc', '.gif', '.gifv', '.mng', '.qt', '.yuv', + '.roq', '.svi', '.viv', '.vp6', '.vp8', '.vp9', '.h264', '.h265', + '.hevc', '.prores', '.dnxhd', '.cineform' + ] - return videoExtensions.includes(extension); - }async validateFiles(filePaths) { - const validFiles = []; + return videoExtensions.includes(extension) + }async validateFiles(filePaths) { + const validFiles = [] - for (const filePath of filePaths) { - try { - const stats = fs.statSync(filePath); - if (stats.isFile()) { - // Detect file type - let mimeType = 'application/octet-stream'; - try { - const fileType = await fileTypeFromFile(filePath); - if (fileType) { - mimeType = fileType.mime; - } else { - // Fallback to extension-based detection for common types - const ext = path.extname(filePath).toLowerCase(); - const mimeMap = { - '.png': 'image/png', - '.jpg': 'image/jpeg', - '.jpeg': 'image/jpeg', - '.gif': 'image/gif', - '.webp': 'image/webp', - '.pdf': 'application/pdf', - '.txt': 'text/plain', - '.doc': 'application/msword', - '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - // Video formats - '.mp4': 'video/mp4', - '.avi': 'video/x-msvideo', - '.mov': 'video/quicktime', - '.wmv': 'video/x-ms-wmv', - '.flv': 'video/x-flv', - '.webm': 'video/webm', - '.mkv': 'video/x-matroska', - '.m4v': 'video/x-m4v', - '.3gp': 'video/3gpp', - '.ogv': 'video/ogg' - }; - mimeType = mimeMap[ext] || 'application/octet-stream'; + for (const filePath of filePaths) { + try { + const stats = fs.statSync(filePath) + if (stats.isFile()) { + // Detect file type + let mimeType = 'application/octet-stream' + try { + const fileType = await fileTypeFromFile(filePath) + if (fileType) { + mimeType = fileType.mime + } else { + // Fallback to extension-based detection for common types + const ext = path.extname(filePath).toLowerCase() + const mimeMap = { + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.webp': 'image/webp', + '.pdf': 'application/pdf', + '.txt': 'text/plain', + '.doc': 'application/msword', + '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + // Video formats + '.mp4': 'video/mp4', + '.avi': 'video/x-msvideo', + '.mov': 'video/quicktime', + '.wmv': 'video/x-ms-wmv', + '.flv': 'video/x-flv', + '.webm': 'video/webm', + '.mkv': 'video/x-matroska', + '.m4v': 'video/x-m4v', + '.3gp': 'video/3gpp', + '.ogv': 'video/ogg' + } + mimeType = mimeMap[ext] || 'application/octet-stream' + } + } catch (error) { + this.log(`Could not detect MIME type for ${filePath}, using fallback`, 'debug') + this.log(`MIME detection error: ${error.message}`, 'debug') + } + + validFiles.push({ + path: filePath, + name: path.basename(filePath), + size: stats.size, + mimeType: mimeType + }) + this.log(`Found file: ${filePath} (${this.formatBytes(stats.size)}, ${mimeType})`, 'debug') + } else { + this.log(`Skipping non-file: ${filePath}`, 'debug') + } + } catch (error) { + this.log(`Cannot access file: ${filePath} - ${error.message}`, 'error') } - } catch (error) { - this.log(`Could not detect MIME type for ${filePath}, using fallback`, 'debug'); - } - - validFiles.push({ - path: filePath, - name: path.basename(filePath), - size: stats.size, - mimeType: mimeType - }); - this.log(`Found file: ${filePath} (${this.formatBytes(stats.size)}, ${mimeType})`, 'debug'); - } else { - this.log(`Skipping non-file: ${filePath}`, 'debug'); } - } catch (error) { - this.log(`Cannot access file: ${filePath} - ${error.message}`, 'error'); - } - } - if (validFiles.length === 0) { - throw new Error('No valid files found to upload'); - } + if (validFiles.length === 0) { + throw new Error('No valid files found to upload') + } - return validFiles; - } - - async generateUploadToken(options = {}) { - const { - context = 'general', - uploadedBy = 'script-user', - maxFiles = 10, - maxSize = 100 * 1024 * 1024, // 100MB - allowedTypes = null, - expiresIn = '15m' - } = options; - - this.log('Generating upload token...', 'debug'); const response = await fetch(`${this.baseUrl}/upload/token`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${this.token}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - context, - uploadedBy, - maxFiles, - maxSize, - allowedTypes, - expiresIn - }) - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`Failed to generate upload token: ${response.status} ${error}`); + return validFiles } - const result = await response.json(); - this.log('Upload token generated successfully', 'debug'); - return result.data; - } async uploadFiles(files, options = {}) { - const { - context = 'general', - uploadedBy = 'script-user', - generateThumbnails = false, - metadata = null, - useUploadToken = false, - forceChunked = false, - chunkThreshold = 25 * 1024 * 1024 // Changed from 100MB to 25MB - } = options; // Check if any files are videos - force chunked upload for all video files - const hasVideoFiles = files.some(file => this.isVideoFile(file)); + async generateUploadToken(options = {}) { + const { + context = 'general', + uploadedBy = 'script-user', + maxFiles = 10, + maxSize = 100 * 1024 * 1024, // 100MB + allowedTypes = null, + expiresIn = '15m' + } = options + + this.log('Generating upload token...', 'debug'); const response = await fetch(`${this.baseUrl}/upload/token`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + context, + uploadedBy, + maxFiles, + maxSize, + allowedTypes, + expiresIn + }) + }) + + if (!response.ok) { + const error = await response.text() + throw new Error(`Failed to generate upload token: ${response.status} ${error}`) + } + + const result = await response.json() + this.log('Upload token generated successfully', 'debug') + return result.data + } async uploadFiles(files, options = {}) { + const { + context = 'general', + uploadedBy = 'script-user', + generateThumbnails = false, + metadata = null, + useUploadToken = false, + forceChunked = false, + chunkThreshold = 25 * 1024 * 1024 // Changed from 100MB to 25MB + } = options // Check if any files are videos - force chunked upload for all video files + const hasVideoFiles = files.some(file => this.isVideoFile(file)) - // Determine if we should use chunked upload - const totalSize = files.reduce((sum, file) => sum + file.size, 0); - const hasLargeFiles = files.some(file => file.size > chunkThreshold); - const shouldUseChunked = forceChunked || hasLargeFiles || hasVideoFiles; - - if (shouldUseChunked) { - if (hasVideoFiles) { - this.log('Using chunked upload for video files', 'info'); - } else { - this.log('Using chunked upload for large files', 'info'); - } - return await this.uploadFilesChunked(files, options); - } + // Determine if we should use chunked upload + // eslint-disable-next-line no-unused-vars + const totalSize = files.reduce((sum, file) => sum + file.size, 0) + const hasLargeFiles = files.some(file => file.size > chunkThreshold) + const shouldUseChunked = forceChunked || hasLargeFiles || hasVideoFiles + + if (shouldUseChunked) { + if (hasVideoFiles) { + this.log('Using chunked upload for video files', 'info') + } else { + this.log('Using chunked upload for large files', 'info') + } + return await this.uploadFilesChunked(files, options) + } - let uploadToken = null; - let authToken = this.token; - - // If using upload token flow, generate token first - if (useUploadToken) { - uploadToken = await this.generateUploadToken({ - context, - uploadedBy, - maxFiles: files.length, - maxSize: files.reduce((sum, file) => sum + file.size, 0) + 1024 * 1024 // Add 1MB buffer - }); - authToken = uploadToken.token; - } + let uploadToken = null + let authToken = this.token + + // If using upload token flow, generate token first + if (useUploadToken) { + uploadToken = await this.generateUploadToken({ + context, + uploadedBy, + maxFiles: files.length, + maxSize: files.reduce((sum, file) => sum + file.size, 0) + 1024 * 1024 // Add 1MB buffer + }) + authToken = uploadToken.token + } + + this.log(`Uploading ${files.length} file(s) to context: ${context}`, 'info') + + // Try curl first for better Cloudflare compatibility + if (this.shouldUseCurl()) { + return await this.uploadWithCurl(files, { + context, + uploadedBy, + generateThumbnails, + metadata, + authToken, + useUploadToken + }) + } - this.log(`Uploading ${files.length} file(s) to context: ${context}`, 'info'); - - // Try curl first for better Cloudflare compatibility - if (this.shouldUseCurl()) { - return await this.uploadWithCurl(files, { - context, - uploadedBy, - generateThumbnails, - metadata, - authToken, - useUploadToken - }); + // Fallback to node-fetch + return await this.uploadWithNodeFetch(files, { + context, + uploadedBy, + generateThumbnails, + metadata, + authToken, + useUploadToken + }) } - // Fallback to node-fetch - return await this.uploadWithNodeFetch(files, { - context, - uploadedBy, - generateThumbnails, - metadata, - authToken, - useUploadToken - }); - } - - shouldUseCurl() { + shouldUseCurl() { // Use curl for remote URLs Cloudflare compatibility) - return !this.baseUrl.includes('localhost') && !this.baseUrl.includes('127.0.0.1'); - } + return !this.baseUrl.includes('localhost') && !this.baseUrl.includes('127.0.0.1') + } - async uploadWithCurl(files, options) { - const { context, uploadedBy, generateThumbnails, metadata, authToken, useUploadToken } = options; + async uploadWithCurl(files, options) { + const { context, uploadedBy, generateThumbnails, metadata, authToken, useUploadToken } = options - try { - // Build curl command - const curlArgs = [ - '-X', 'POST', - '-H', `Authorization: Bearer ${authToken}`, - '-s', // Silent mode - '--fail-with-body' // Return error body on failure - ]; - - // Add files - for (const file of files) { - curlArgs.push('-F', `files=@${file.path}`); - } - - // Add form fields - if (!useUploadToken) { - curlArgs.push('-F', `context=${context}`); - curlArgs.push('-F', `uploadedBy=${uploadedBy}`); - } - - if (generateThumbnails) { - curlArgs.push('-F', 'generateThumbnails=true'); - } - - if (metadata) { - curlArgs.push('-F', `metadata=${JSON.stringify(metadata)}`); - } - - curlArgs.push(`${this.baseUrl}/upload`); - - this.log('Using curl for upload (better tunnel compatibility)', 'debug'); + try { + // Build curl command + const curlArgs = [ + '-X', 'POST', + '-H', `Authorization: Bearer ${authToken}`, + '-s', // Silent mode + '--fail-with-body' // Return error body on failure + ] + + // Add files + for (const file of files) { + curlArgs.push('-F', `files=@${file.path}`) + } + + // Add form fields + if (!useUploadToken) { + curlArgs.push('-F', `context=${context}`) + curlArgs.push('-F', `uploadedBy=${uploadedBy}`) + } + + if (generateThumbnails) { + curlArgs.push('-F', 'generateThumbnails=true') + } + + if (metadata) { + curlArgs.push('-F', `metadata=${JSON.stringify(metadata)}`) + } + + curlArgs.push(`${this.baseUrl}/upload`) + + this.log('Using curl for upload (better tunnel compatibility)', 'debug') - const result = await this.executeCurl(curlArgs); - const response = JSON.parse(result); + const result = await this.executeCurl(curlArgs) + const response = JSON.parse(result) - if (response.success) { - return response.data; - } else { - throw new Error(response.error || 'Upload failed'); - } + if (response.success) { + return response.data + } else { + throw new Error(response.error || 'Upload failed') + } - } catch (error) { - this.log(`Curl upload failed: ${error.message}`, 'error'); - this.log('Falling back to node-fetch...', 'debug'); + } catch (error) { + this.log(`Curl upload failed: ${error.message}`, 'error') + this.log('Falling back to node-fetch...', 'debug') - // Fallback to node-fetch - return await this.uploadWithNodeFetch(files, options); + // Fallback to node-fetch + return await this.uploadWithNodeFetch(files, options) + } } - } - async executeCurl(args) { - return new Promise((resolve, reject) => { - const curl = spawn('curl', args, { - stdio: ['pipe', 'pipe', 'pipe'] - }); + async executeCurl(args) { + return new Promise((resolve, reject) => { + const curl = spawn('curl', args, { + stdio: ['pipe', 'pipe', 'pipe'] + }) - let stdout = ''; - let stderr = ''; + let stdout = '' + let stderr = '' - curl.stdout.on('data', (data) => { - stdout += data.toString(); - }); + curl.stdout.on('data', (data) => { + stdout += data.toString() + }) - curl.stderr.on('data', (data) => { - stderr += data.toString(); - }); + curl.stderr.on('data', (data) => { + stderr += data.toString() + }) - curl.on('close', (code) => { - if (code === 0) { - resolve(stdout); - } else { - reject(new Error(`curl failed with code ${code}: ${stderr}`)); - } - }); + curl.on('close', (code) => { + if (code === 0) { + resolve(stdout) + } else { + reject(new Error(`curl failed with code ${code}: ${stderr}`)) + } + }) - curl.on('error', (error) => { - reject(new Error(`curl execution failed: ${error.message}`)); - }); - }); - } + curl.on('error', (error) => { + reject(new Error(`curl execution failed: ${error.message}`)) + }) + }) + } - async uploadWithNodeFetch(files, options) { - const { context, uploadedBy, generateThumbnails, metadata, authToken, useUploadToken } = options; + async uploadWithNodeFetch(files, options) { + const { context, uploadedBy, generateThumbnails, metadata, authToken, useUploadToken } = options - const form = new FormData(); + const form = new FormData() - // Add files to form data - for (const file of files) { - form.append('files', fs.createReadStream(file.path), { - filename: file.name, - contentType: file.mimeType || 'application/octet-stream' // Use detected MIME type - }); - } + // Add files to form data + for (const file of files) { + form.append('files', fs.createReadStream(file.path), { + filename: file.name, + contentType: file.mimeType || 'application/octet-stream' // Use detected MIME type + }) + } - // Add additional fields - if (!useUploadToken) { - form.append('context', context); - form.append('uploadedBy', uploadedBy); - } + // Add additional fields + if (!useUploadToken) { + form.append('context', context) + form.append('uploadedBy', uploadedBy) + } - if (generateThumbnails) { - form.append('generateThumbnails', 'true'); - } + if (generateThumbnails) { + form.append('generateThumbnails', 'true') + } - if (metadata) { - form.append('metadata', JSON.stringify(metadata)); - } + if (metadata) { + form.append('metadata', JSON.stringify(metadata)) + } - try { - const response = await fetch(`${this.baseUrl}/upload`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${authToken}`, - ...form.getHeaders() - }, - body: form - }); - - if (!response.ok) { - const errorText = await response.text(); - let errorData; try { - errorData = JSON.parse(errorText); - } catch { - errorData = { error: errorText }; - } - throw new Error(`Upload failed: ${response.status} - ${errorData.error || errorText}`); - } + const response = await fetch(`${this.baseUrl}/upload`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${authToken}`, + ...form.getHeaders() + }, + body: form + }) - const result = await response.json(); - return result.data; + if (!response.ok) { + const errorText = await response.text() + let errorData + try { + errorData = JSON.parse(errorText) + } catch { + errorData = { error: errorText } + } + throw new Error(`Upload failed: ${response.status} - ${errorData.error || errorText}`) + } - } catch (error) { - this.log(`Upload error: ${error.message}`, 'error'); - throw error; + const result = await response.json() + return result.data + + } catch (error) { + this.log(`Upload error: ${error.message}`, 'error') + throw error + } } - } - /** + /** * Upload files using chunked upload API */ - async uploadFilesChunked(files, options = {}) { - const { - context = 'general', - uploadedBy = 'script-user', - generateThumbnails = false, - metadata = null, - useUploadToken = false - } = options; - - let uploadToken = null; - let authToken = this.token; - - // If using upload token flow, generate token first - if (useUploadToken) { - uploadToken = await this.generateUploadToken({ - context, - uploadedBy, - maxFiles: files.length, - maxSize: files.reduce((sum, file) => sum + file.size, 0) + 1024 * 1024 // Add 1MB buffer - }); - authToken = uploadToken.token; - } + async uploadFilesChunked(files, options = {}) { + const { + context = 'general', + uploadedBy = 'script-user', + generateThumbnails = false, + metadata = null, + useUploadToken = false + } = options + + let uploadToken = null + let authToken = this.token + + // If using upload token flow, generate token first + if (useUploadToken) { + uploadToken = await this.generateUploadToken({ + context, + uploadedBy, + maxFiles: files.length, + maxSize: files.reduce((sum, file) => sum + file.size, 0) + 1024 * 1024 // Add 1MB buffer + }) + authToken = uploadToken.token + } - const results = []; + const results = [] - for (const file of files) { - this.log(`Starting chunked upload for: ${file.name} (${this.formatBytes(file.size)})`, 'info'); + for (const file of files) { + this.log(`Starting chunked upload for: ${file.name} (${this.formatBytes(file.size)})`, 'info') - try { const result = await this.uploadSingleFileChunked(file, { - context, - uploadedBy, - generateThumbnails, - metadata, - authToken, - useUploadToken, - maxRetries: options.maxRetries, - chunkTimeout: options.chunkTimeout, - maxConcurrent: options.maxConcurrent - }); + try { const result = await this.uploadSingleFileChunked(file, { + context, + uploadedBy, + generateThumbnails, + metadata, + authToken, + useUploadToken, + maxRetries: options.maxRetries, + chunkTimeout: options.chunkTimeout, + maxConcurrent: options.maxConcurrent + }) - results.push(result); - this.log(`āœ“ Completed: ${file.name}`, 'success'); + results.push(result) + this.log(`āœ“ Completed: ${file.name}`, 'success') - } catch (error) { - this.log(`āœ— Failed: ${file.name} - ${error.message}`, 'error'); - throw error; - } - } + } catch (error) { + this.log(`āœ— Failed: ${file.name} - ${error.message}`, 'error') + throw error + } + } - return { - files: results, - count: results.length, - totalSize: results.reduce((sum, file) => sum + file.fileSize, 0) - }; - } - /** + return { + files: results, + count: results.length, + totalSize: results.reduce((sum, file) => sum + file.fileSize, 0) + } + } + /** * Upload a single file using chunked upload */ - async uploadSingleFileChunked(file, options) { - const { - context, - uploadedBy, - generateThumbnails, - metadata, - authToken, - useUploadToken, - maxRetries = 3, - chunkTimeout = 120, - maxConcurrent = 2 - } = options; - - // Step 1: Initialize upload session - const sessionData = await this.initializeChunkedUpload(file, { - context, - uploadedBy, - metadata, - authToken, - useUploadToken - }); - - const { sessionId, chunkSize, totalChunks } = sessionData; - - this.log(`Initialized session ${sessionId} with ${totalChunks} chunks of ${this.formatBytes(chunkSize)}`, 'debug'); // Step 2: Upload chunks - await this.uploadChunks(file, sessionId, chunkSize, totalChunks, authToken, { - maxRetries, - chunkTimeout, - maxConcurrent - }); - - // Small delay to ensure all chunks are fully processed - this.log('All chunks uploaded, waiting for server processing...', 'debug'); - await new Promise(resolve => setTimeout(resolve, 1000)); - - // Step 3: Complete upload - const result = await this.completeChunkedUpload(sessionId, authToken); - - return result.file; - } - /** + async uploadSingleFileChunked(file, options) { + const { + context, + uploadedBy, + // eslint-disable-next-line no-unused-vars + generateThumbnails, + metadata, + authToken, + useUploadToken, + maxRetries = 3, + chunkTimeout = 120, + maxConcurrent = 2 + } = options + + // Step 1: Initialize upload session + const sessionData = await this.initializeChunkedUpload(file, { + context, + uploadedBy, + metadata, + authToken, + useUploadToken + }) + + const { sessionId, chunkSize, totalChunks } = sessionData + + this.log(`Initialized session ${sessionId} with ${totalChunks} chunks of ${this.formatBytes(chunkSize)}`, 'debug') // Step 2: Upload chunks + await this.uploadChunks(file, sessionId, chunkSize, totalChunks, authToken, { + maxRetries, + chunkTimeout, + maxConcurrent + }) + + // Small delay to ensure all chunks are fully processed + this.log('All chunks uploaded, waiting for server processing...', 'debug') + await new Promise(resolve => setTimeout(resolve, 1000)) + + // Step 3: Complete upload + const result = await this.completeChunkedUpload(sessionId, authToken) + + return result.file + } + /** * Initialize chunked upload session */ - async initializeChunkedUpload(file, options) { - const { context, uploadedBy, metadata, authToken, useUploadToken } = options; + async initializeChunkedUpload(file, options) { + const { context, uploadedBy, metadata, authToken, useUploadToken } = options - this.log(`Initializing chunked upload for: ${file.name} (${this.formatBytes(file.size)})`, 'debug'); + this.log(`Initializing chunked upload for: ${file.name} (${this.formatBytes(file.size)})`, 'debug') - const payload = { - originalFilename: file.name, - totalSize: file.size, - mimeType: file.mimeType - }; + const payload = { + originalFilename: file.name, + totalSize: file.size, + mimeType: file.mimeType + } - if (!useUploadToken) { - payload.context = context; - payload.uploadedBy = uploadedBy; - } + if (!useUploadToken) { + payload.context = context + payload.uploadedBy = uploadedBy + } - if (metadata) { - payload.metadata = metadata; - } + if (metadata) { + payload.metadata = metadata + } - this.log(`Sending init request to: ${this.baseUrl}/upload/chunk/init`, 'debug'); - - const response = await fetch(`${this.baseUrl}/upload/chunk/init`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${authToken}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify(payload) - }); - - if (!response.ok) { - const error = await response.text(); - this.log(`Init failed with status ${response.status}: ${error}`, 'error'); - throw new Error(`Failed to initialize chunked upload: ${response.status} ${error}`); - } + this.log(`Sending init request to: ${this.baseUrl}/upload/chunk/init`, 'debug') + + const response = await fetch(`${this.baseUrl}/upload/chunk/init`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${authToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }) - const result = await response.json(); - this.log(`Session initialized successfully: ${result.data.sessionId}`, 'debug'); - return result.data; - }/** + if (!response.ok) { + const error = await response.text() + this.log(`Init failed with status ${response.status}: ${error}`, 'error') + throw new Error(`Failed to initialize chunked upload: ${response.status} ${error}`) + } + + const result = await response.json() + this.log(`Session initialized successfully: ${result.data.sessionId}`, 'debug') + return result.data + }/** * Upload all chunks for a file */ - async uploadChunks(file, sessionId, chunkSize, totalChunks, authToken, options = {}) { - const { maxRetries = 3, chunkTimeout = 120, maxConcurrent = 2 } = options; - const filePath = file.path; - const fileHandle = fs.openSync(filePath, 'r'); + async uploadChunks(file, sessionId, chunkSize, totalChunks, authToken, options = {}) { + const { maxRetries = 3, chunkTimeout = 120, maxConcurrent = 2 } = options + const filePath = file.path + const fileHandle = fs.openSync(filePath, 'r') - try { - // Upload chunks with controlled concurrency for better Cloudflare compatibility - let currentChunk = 0; - const activeUploads = new Set(); + try { + // Upload chunks with controlled concurrency for better Cloudflare compatibility + let currentChunk = 0 + const activeUploads = new Set() - // ETA tracking - const startTime = Date.now(); - let completedChunks = 0; - let totalBytesUploaded = 0; - - while (currentChunk < totalChunks) { - // Start uploads up to maxConcurrent - while (activeUploads.size < maxConcurrent && currentChunk < totalChunks) { - const chunkIndex = currentChunk++; - const uploadPromise = this.uploadSingleChunk( - fileHandle, - sessionId, - chunkIndex, - chunkSize, - authToken, - maxRetries, - chunkTimeout * 1000 // Convert to milliseconds - ); + // ETA tracking + const startTime = Date.now() + let completedChunks = 0 + let totalBytesUploaded = 0 + + while (currentChunk < totalChunks) { + // Start uploads up to maxConcurrent + while (activeUploads.size < maxConcurrent && currentChunk < totalChunks) { + const chunkIndex = currentChunk++ + const uploadPromise = this.uploadSingleChunk( + fileHandle, + sessionId, + chunkIndex, + chunkSize, + authToken, + maxRetries, + chunkTimeout * 1000 // Convert to milliseconds + ) - activeUploads.add(uploadPromise); + activeUploads.add(uploadPromise) - uploadPromise - .then((result) => { - activeUploads.delete(uploadPromise); - completedChunks++; - totalBytesUploaded += result.chunkSize || chunkSize; + uploadPromise + .then((result) => { + activeUploads.delete(uploadPromise) + completedChunks++ + totalBytesUploaded += result.chunkSize || chunkSize - // Calculate ETA - const elapsed = Date.now() - startTime; - const progress = completedChunks / totalChunks; - const eta = progress > 0 ? (elapsed / progress) - elapsed : 0; - const etaFormatted = this.formatDuration(eta); - const speed = this.formatBytes(totalBytesUploaded / (elapsed / 1000)) + '/s'; + // Calculate ETA + const elapsed = Date.now() - startTime + const progress = completedChunks / totalChunks + const eta = progress > 0 ? (elapsed / progress) - elapsed : 0 + const etaFormatted = this.formatDuration(eta) + const speed = this.formatBytes(totalBytesUploaded / (elapsed / 1000)) + '/s' - const progressPercent = Math.round(progress * 100); - this.log(`Chunk ${chunkIndex + 1}/${totalChunks} uploaded (${progressPercent}%) - Speed: ${speed} - ETA: ${etaFormatted}`, 'debug'); - }) - .catch(error => { - activeUploads.delete(uploadPromise); - throw error; - }); - } - - // Wait for at least one upload to complete before starting more - if (activeUploads.size > 0) { - await Promise.race(activeUploads); - } - } + const progressPercent = Math.round(progress * 100) + this.log(`Chunk ${chunkIndex + 1}/${totalChunks} uploaded (${progressPercent}%) - Speed: ${speed} - ETA: ${etaFormatted}`, 'debug') + }) + .catch(error => { + activeUploads.delete(uploadPromise) + throw error + }) + } + + // Wait for at least one upload to complete before starting more + if (activeUploads.size > 0) { + await Promise.race(activeUploads) + } + } - // Wait for all remaining uploads to complete - await Promise.all(activeUploads); + // Wait for all remaining uploads to complete + await Promise.all(activeUploads) - } finally { - fs.closeSync(fileHandle); - } - } /** + } finally { + fs.closeSync(fileHandle) + } + } /** * Upload a single chunk with retry logic */ - async uploadSingleChunk(fileHandle, sessionId, chunkIndex, chunkSize, authToken, maxRetries = 3, timeoutMs = 120000) { - const buffer = Buffer.alloc(chunkSize); - const position = chunkIndex * chunkSize; - const bytesRead = fs.readSync(fileHandle, buffer, 0, chunkSize, position); + async uploadSingleChunk(fileHandle, sessionId, chunkIndex, chunkSize, authToken, maxRetries = 3, timeoutMs = 120000) { + const buffer = Buffer.alloc(chunkSize) + const position = chunkIndex * chunkSize + const bytesRead = fs.readSync(fileHandle, buffer, 0, chunkSize, position) - // Trim buffer to actual bytes read for last chunk - const chunkData = bytesRead < chunkSize ? buffer.slice(0, bytesRead) : buffer; - - for (let attempt = 1; attempt <= maxRetries; attempt++) { - try { - const form = new FormData(); - form.append('chunk', chunkData, { - filename: `chunk-${chunkIndex}`, - contentType: 'application/octet-stream' - }); // Create AbortController for timeout - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeoutMs); - - this.log(`Uploading chunk ${chunkIndex} to: ${this.baseUrl}/upload/chunk/${sessionId}/${chunkIndex}`, 'debug'); - - const response = await fetch(`${this.baseUrl}/upload/chunk/${sessionId}/${chunkIndex}`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${authToken}`, - ...form.getHeaders() - }, - body: form, - signal: controller.signal - }); - - clearTimeout(timeoutId); - - if (!response.ok) { - const error = await response.text().catch(() => 'Unknown error'); + // Trim buffer to actual bytes read for last chunk + const chunkData = bytesRead < chunkSize ? buffer.slice(0, bytesRead) : buffer + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + const form = new FormData() + form.append('chunk', chunkData, { + filename: `chunk-${chunkIndex}`, + contentType: 'application/octet-stream' + }) // Create AbortController for timeout + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), timeoutMs) + + this.log(`Uploading chunk ${chunkIndex} to: ${this.baseUrl}/upload/chunk/${sessionId}/${chunkIndex}`, 'debug') + + const response = await fetch(`${this.baseUrl}/upload/chunk/${sessionId}/${chunkIndex}`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${authToken}`, + ...form.getHeaders() + }, + body: form, + signal: controller.signal + }) + + clearTimeout(timeoutId) + + if (!response.ok) { + const error = await response.text().catch(() => 'Unknown error') - // Log the specific error - if (response.status === 524) { - this.log(`Chunk ${chunkIndex} timeout (524) - attempt ${attempt}/${maxRetries}`, 'error'); - } else { - this.log(`Chunk ${chunkIndex} failed with ${response.status} - attempt ${attempt}/${maxRetries}`, 'error'); - } + // Log the specific error + if (response.status === 524) { + this.log(`Chunk ${chunkIndex} timeout (524) - attempt ${attempt}/${maxRetries}`, 'error') + } else { + this.log(`Chunk ${chunkIndex} failed with ${response.status} - attempt ${attempt}/${maxRetries}`, 'error') + } - if (attempt === maxRetries) { - throw new Error(`Failed to upload chunk ${chunkIndex}: ${response.status} ${error}`); - } + if (attempt === maxRetries) { + throw new Error(`Failed to upload chunk ${chunkIndex}: ${response.status} ${error}`) + } - // Exponential backoff - const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000); - this.log(`Retrying chunk ${chunkIndex} in ${delay}ms...`, 'debug'); - await new Promise(resolve => setTimeout(resolve, delay)); - continue; - } - - const result = await response.json(); - return { ...result.data, chunkSize: bytesRead }; - - } catch (error) { - if (error.name === 'AbortError') { - this.log(`Chunk ${chunkIndex} upload timed out - attempt ${attempt}/${maxRetries}`, 'error'); - } else { - this.log(`Chunk ${chunkIndex} upload error: ${error.message} - attempt ${attempt}/${maxRetries}`, 'error'); - } + // Exponential backoff + const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000) + this.log(`Retrying chunk ${chunkIndex} in ${delay}ms...`, 'debug') + await new Promise(resolve => setTimeout(resolve, delay)) + continue + } + + const result = await response.json() + return { ...result.data, chunkSize: bytesRead } + + } catch (error) { + if (error.name === 'AbortError') { + this.log(`Chunk ${chunkIndex} upload timed out - attempt ${attempt}/${maxRetries}`, 'error') + } else { + this.log(`Chunk ${chunkIndex} upload error: ${error.message} - attempt ${attempt}/${maxRetries}`, 'error') + } - if (attempt === maxRetries) { - throw new Error(`Failed to upload chunk ${chunkIndex} after ${maxRetries} attempts: ${error.message}`); - } + if (attempt === maxRetries) { + throw new Error(`Failed to upload chunk ${chunkIndex} after ${maxRetries} attempts: ${error.message}`) + } - // Exponential backoff - const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000); - this.log(`Retrying chunk ${chunkIndex} in ${delay}ms...`, 'debug'); - await new Promise(resolve => setTimeout(resolve, delay)); - } - } - } /** + // Exponential backoff + const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000) + this.log(`Retrying chunk ${chunkIndex} in ${delay}ms...`, 'debug') + await new Promise(resolve => setTimeout(resolve, delay)) + } + } + } /** * Complete chunked upload with retry logic */ - async completeChunkedUpload(sessionId, authToken, maxRetries = 3) { - this.log(`Completing chunked upload for session: ${sessionId}`, 'debug'); + async completeChunkedUpload(sessionId, authToken, maxRetries = 3) { + this.log(`Completing chunked upload for session: ${sessionId}`, 'debug') - for (let attempt = 1; attempt <= maxRetries; attempt++) { - try { - const response = await fetch(`${this.baseUrl}/upload/chunk/${sessionId}/complete`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${authToken}` - } - }); - - if (!response.ok) { - const error = await response.text(); + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + const response = await fetch(`${this.baseUrl}/upload/chunk/${sessionId}/complete`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${authToken}` + } + }) + + if (!response.ok) { + const error = await response.text() - // If it's a missing chunks error, check status and retry - if (response.status === 400 && error.includes('Missing chunk')) { - this.log(`Missing chunks detected, checking status...`, 'debug'); + // If it's a missing chunks error, check status and retry + if (response.status === 400 && error.includes('Missing chunk')) { + this.log('Missing chunks detected, checking status...', 'debug') - // Check session status - const statusResponse = await fetch(`${this.baseUrl}/upload/chunk/${sessionId}/status`, { - headers: { 'Authorization': `Bearer ${authToken}` } - }); + // Check session status + const statusResponse = await fetch(`${this.baseUrl}/upload/chunk/${sessionId}/status`, { + headers: { 'Authorization': `Bearer ${authToken}` } + }) - if (statusResponse.ok) { - const status = await statusResponse.json(); - this.log(`Session status: ${status.data.uploadedChunks}/${status.data.totalChunks} chunks uploaded`, 'debug'); + if (statusResponse.ok) { + const status = await statusResponse.json() + this.log(`Session status: ${status.data.uploadedChunks}/${status.data.totalChunks} chunks uploaded`, 'debug') - if (status.data.missingChunks.length > 0) { - this.log(`Missing chunks: ${status.data.missingChunks.join(', ')}`, 'error'); - throw new Error(`Missing chunks: ${status.data.missingChunks.join(', ')}`); - } - } - } + if (status.data.missingChunks.length > 0) { + this.log(`Missing chunks: ${status.data.missingChunks.join(', ')}`, 'error') + throw new Error(`Missing chunks: ${status.data.missingChunks.join(', ')}`) + } + } + } - this.log(`Complete upload failed with status ${response.status}: ${error} - attempt ${attempt}/${maxRetries}`, 'error'); + this.log(`Complete upload failed with status ${response.status}: ${error} - attempt ${attempt}/${maxRetries}`, 'error') - if (attempt === maxRetries) { - throw new Error(`Failed to complete chunked upload: ${response.status} ${error}`); - } + if (attempt === maxRetries) { + throw new Error(`Failed to complete chunked upload: ${response.status} ${error}`) + } - // Wait before retry - const delay = 2000 * attempt; // 2s, 4s, 6s - this.log(`Retrying completion in ${delay}ms...`, 'debug'); - await new Promise(resolve => setTimeout(resolve, delay)); - continue; - } - - const result = await response.json(); - this.log(`Chunked upload completed successfully for session: ${sessionId}`, 'debug'); - return result.data; + // Wait before retry + const delay = 2000 * attempt // 2s, 4s, 6s + this.log(`Retrying completion in ${delay}ms...`, 'debug') + await new Promise(resolve => setTimeout(resolve, delay)) + continue + } + + const result = await response.json() + this.log(`Chunked upload completed successfully for session: ${sessionId}`, 'debug') + return result.data - } catch (error) { - if (attempt === maxRetries) { - throw error; - } + } catch (error) { + if (attempt === maxRetries) { + throw error + } - this.log(`Completion attempt ${attempt} failed: ${error.message}`, 'error'); - const delay = 2000 * attempt; - this.log(`Retrying completion in ${delay}ms...`, 'debug'); - await new Promise(resolve => setTimeout(resolve, delay)); - } + this.log(`Completion attempt ${attempt} failed: ${error.message}`, 'error') + const delay = 2000 * attempt + this.log(`Retrying completion in ${delay}ms...`, 'debug') + await new Promise(resolve => setTimeout(resolve, delay)) + } + } } - } - async testConnection() { - try { - this.log('Testing connection to Otto server...', 'debug'); + async testConnection() { + try { + this.log('Testing connection to Otto server...', 'debug') - const response = await fetch(`${this.baseUrl}/health`); - if (!response.ok) { - throw new Error(`Server health check failed: ${response.status}`); - } - - const health = await response.json(); - this.log(`Connected to Otto server (${health.status})`, 'debug'); - return true; - } catch (error) { - this.log(`Connection test failed: ${error.message}`, 'error'); - return false; - } - } - async getUploadStats() { - try { - const response = await fetch(`${this.baseUrl}/stats`, { - headers: { - 'Authorization': `Bearer ${this.token}` + const response = await fetch(`${this.baseUrl}/health`) + if (!response.ok) { + throw new Error(`Server health check failed: ${response.status}`) + } + + const health = await response.json() + this.log(`Connected to Otto server (${health.status})`, 'debug') + return true + } catch (error) { + this.log(`Connection test failed: ${error.message}`, 'error') + return false } - }); + } + async getUploadStats() { + try { + const response = await fetch(`${this.baseUrl}/stats`, { + headers: { + 'Authorization': `Bearer ${this.token}` + } + }) - if (!response.ok) { - throw new Error(`Failed to get stats: ${response.status}`); - } + if (!response.ok) { + throw new Error(`Failed to get stats: ${response.status}`) + } - const result = await response.json(); - return result.data; - } catch (error) { - this.log(`Failed to get upload stats: ${error.message}`, 'error'); - return null; + const result = await response.json() + return result.data + } catch (error) { + this.log(`Failed to get upload stats: ${error.message}`, 'error') + return null + } } - } } // CLI Setup -const program = new Command(); +const program = new Command() program - .name('otto-upload') - .description('Upload files to Otto server') - .version('1.0.0') - .argument('', 'Files to upload (supports glob patterns)') - .option('-u, --url ', 'Otto server URL', 'http://localhost:3000') - .option('-t, --token ', 'Service token or upload token') - .option('-c, --context ', 'Upload context', 'general') - .option('-b, --uploaded-by ', 'User ID for upload attribution', 'script-user') - .option('-m, --metadata ', 'Additional metadata as JSON string') - .option('--thumbnails', 'Generate thumbnails for images', false) - .option('--upload-token', 'Use upload token flow (requires service token)', false) .option('--chunked', 'Force chunked upload for all files', false) - .option('--chunk-threshold ', 'File size threshold for auto chunked upload (MB) - videos always use chunked', '25') - .option('--max-retries ', 'Maximum retry attempts for failed chunks', '3') - .option('--chunk-timeout ', 'Timeout for individual chunk uploads (seconds)', '120') - .option('--max-concurrent ', 'Maximum concurrent chunk uploads', '2') - .option('-v, --verbose', 'Verbose logging', false) - .option('--stats', 'Show upload statistics after upload', false) - .option('--test-only', 'Only test connection without uploading', false); + .name('otto-upload') + .description('Upload files to Otto server') + .version('1.0.0') + .argument('', 'Files to upload (supports glob patterns)') + .option('-u, --url ', 'Otto server URL', 'http://localhost:3000') + .option('-t, --token ', 'Service token or upload token') + .option('-c, --context ', 'Upload context', 'general') + .option('-b, --uploaded-by ', 'User ID for upload attribution', 'script-user') + .option('-m, --metadata ', 'Additional metadata as JSON string') + .option('--thumbnails', 'Generate thumbnails for images', false) + .option('--upload-token', 'Use upload token flow (requires service token)', false) .option('--chunked', 'Force chunked upload for all files', false) + .option('--chunk-threshold ', 'File size threshold for auto chunked upload (MB) - videos always use chunked', '25') + .option('--max-retries ', 'Maximum retry attempts for failed chunks', '3') + .option('--chunk-timeout ', 'Timeout for individual chunk uploads (seconds)', '120') + .option('--max-concurrent ', 'Maximum concurrent chunk uploads', '2') + .option('-v, --verbose', 'Verbose logging', false) + .option('--stats', 'Show upload statistics after upload', false) + .option('--test-only', 'Only test connection without uploading', false) program.action(async (files, options) => { - try { + try { // Validate token - if (!options.token) { - console.error(chalk.red('Error: Token is required. Use --token option or set OTTO_TOKEN environment variable.')); - process.exit(1); - } + if (!options.token) { + console.error(chalk.red('Error: Token is required. Use --token option or set OTTO_TOKEN environment variable.')) + process.exit(1) + } - // Parse metadata if provided - let metadata = null; - if (options.metadata) { - try { - metadata = JSON.parse(options.metadata); - } catch (error) { - console.error(chalk.red(`Error: Invalid metadata JSON - ${error.message}`)); - process.exit(1); - } - } + // Parse metadata if provided + let metadata = null + if (options.metadata) { + try { + metadata = JSON.parse(options.metadata) + } catch (error) { + console.error(chalk.red(`Error: Invalid metadata JSON - ${error.message}`)) + process.exit(1) + } + } - // Create uploader instance - const uploader = new OttoUploader({ - baseUrl: options.url, - token: options.token, - verbose: options.verbose - }); - - // Test connection - const connected = await uploader.testConnection(); - if (!connected) { - console.error(chalk.red('Error: Cannot connect to Otto server')); - process.exit(1); - } + // Create uploader instance + const uploader = new OttoUploader({ + baseUrl: options.url, + token: options.token, + verbose: options.verbose + }) + + // Test connection + const connected = await uploader.testConnection() + if (!connected) { + console.error(chalk.red('Error: Cannot connect to Otto server')) + process.exit(1) + } - if (options.testOnly) { - console.log(chalk.green('āœ“ Connection test successful')); - return; - } + if (options.testOnly) { + console.log(chalk.green('āœ“ Connection test successful')) + return + } - // Validate and prepare files - const validFiles = await uploader.validateFiles(files); - const totalSize = validFiles.reduce((sum, file) => sum + file.size, 0); + // Validate and prepare files + const validFiles = await uploader.validateFiles(files) + const totalSize = validFiles.reduce((sum, file) => sum + file.size, 0) - uploader.log(`Preparing to upload ${validFiles.length} files (total: ${uploader.formatBytes(totalSize)})`, 'info'); // Upload files - const uploadResult = await uploader.uploadFiles(validFiles, { - context: options.context, - uploadedBy: options.uploadedBy, - generateThumbnails: options.thumbnails, - metadata, - useUploadToken: options.uploadToken, - forceChunked: options.chunked, - chunkThreshold: parseInt(options.chunkThreshold) * 1024 * 1024, // Convert MB to bytes - maxRetries: parseInt(options.maxRetries), - chunkTimeout: parseInt(options.chunkTimeout), - maxConcurrent: parseInt(options.maxConcurrent) - }); - - // Display results - console.log(chalk.green('\nāœ“ Upload completed successfully!')); - console.log(chalk.cyan(`\nUploaded Files (${uploadResult.files.length}):`)); + uploader.log(`Preparing to upload ${validFiles.length} files (total: ${uploader.formatBytes(totalSize)})`, 'info') // Upload files + const uploadResult = await uploader.uploadFiles(validFiles, { + context: options.context, + uploadedBy: options.uploadedBy, + generateThumbnails: options.thumbnails, + metadata, + useUploadToken: options.uploadToken, + forceChunked: options.chunked, + chunkThreshold: parseInt(options.chunkThreshold) * 1024 * 1024, // Convert MB to bytes + maxRetries: parseInt(options.maxRetries), + chunkTimeout: parseInt(options.chunkTimeout), + maxConcurrent: parseInt(options.maxConcurrent) + }) + + // Display results + console.log(chalk.green('\nāœ“ Upload completed successfully!')) + console.log(chalk.cyan(`\nUploaded Files (${uploadResult.files.length}):`)) - uploadResult.files.forEach((file, index) => { - console.log(chalk.white(`${index + 1}. ${file.originalName}`)); - console.log(chalk.gray(` ID: ${file.id}`)); - console.log(chalk.gray(` Size: ${uploader.formatBytes(file.fileSize)}`)); - console.log(chalk.gray(` Type: ${file.mimeType}`)); - console.log(chalk.gray(` URL: ${file.url}`)); - if (file.publicUrl) { - console.log(chalk.gray(` Public URL: ${file.publicUrl}`)); - } - console.log(''); - }); - - console.log(chalk.cyan(`Total uploaded: ${uploader.formatBytes(uploadResult.totalSize)}`)); - - // Show stats if requested - if (options.stats) { - const stats = await uploader.getUploadStats(); - if (stats) { - console.log(chalk.cyan('\nServer Statistics:')); - console.log(chalk.white(`Total Files: ${stats.totalFiles}`)); - console.log(chalk.white(`Total Size: ${stats.formattedTotalSize}`)); - console.log(chalk.white(`Unique Contexts: ${stats.uniqueContexts}`)); - console.log(chalk.white(`Unique Uploaders: ${stats.uniqueUploaders}`)); - } } - - // Exit successfully - process.exit(0); - - } catch (error) { - console.error(chalk.red(`\nError: ${error.message}`)); - process.exit(1); - } -}); + uploadResult.files.forEach((file, index) => { + console.log(chalk.white(`${index + 1}. ${file.originalName}`)) + console.log(chalk.gray(` ID: ${file.id}`)) + console.log(chalk.gray(` Size: ${uploader.formatBytes(file.fileSize)}`)) + console.log(chalk.gray(` Type: ${file.mimeType}`)) + console.log(chalk.gray(` URL: ${file.url}`)) + if (file.publicUrl) { + console.log(chalk.gray(` Public URL: ${file.publicUrl}`)) + } + console.log('') + }) + + console.log(chalk.cyan(`Total uploaded: ${uploader.formatBytes(uploadResult.totalSize)}`)) + + // Show stats if requested + if (options.stats) { + const stats = await uploader.getUploadStats() + if (stats) { + console.log(chalk.cyan('\nServer Statistics:')) + console.log(chalk.white(`Total Files: ${stats.totalFiles}`)) + console.log(chalk.white(`Total Size: ${stats.formattedTotalSize}`)) + console.log(chalk.white(`Unique Contexts: ${stats.uniqueContexts}`)) + console.log(chalk.white(`Unique Uploaders: ${stats.uniqueUploaders}`)) + } } + + // Exit successfully + process.exit(0) + + } catch (error) { + console.error(chalk.red(`\nError: ${error.message}`)) + process.exit(1) + } +}) // Handle environment variables if (!process.argv.includes('--token') && !process.argv.includes('-t')) { - const envToken = process.env.OTTO_TOKEN || process.env.OTTO_SERVICE_TOKEN; - if (envToken) { - process.argv.push('--token', envToken); - } + const envToken = process.env.OTTO_TOKEN || process.env.OTTO_SERVICE_TOKEN + if (envToken) { + process.argv.push('--token', envToken) + } } if (!process.argv.includes('--url') && !process.argv.includes('-u')) { - const envUrl = process.env.OTTO_URL || process.env.OTTO_BASE_URL; - if (envUrl) { - process.argv.push('--url', envUrl); - } + const envUrl = process.env.OTTO_URL || process.env.OTTO_BASE_URL + if (envUrl) { + process.argv.push('--url', envUrl) + } } // Parse and run -program.parse(); +program.parse() diff --git a/src/config/database.js b/src/config/database.js index 7023582..651b932 100644 --- a/src/config/database.js +++ b/src/config/database.js @@ -1,100 +1,100 @@ -import pg from 'pg'; -import logger from './logger.js'; +import pg from 'pg' +import logger from './logger.js' -const { Pool } = pg; +const { Pool } = pg class Database { - constructor() { - this.pool = null; - } + constructor() { + this.pool = null + } - getPool() { - if (!this.pool) { - const config = { - host: process.env.DB_HOST || 'localhost', - port: parseInt(process.env.DB_PORT) || 5432, - database: process.env.DB_NAME || 'otto', - user: process.env.DB_USER || 'otto_user', - password: process.env.DB_PASSWORD, - max: 20, - idleTimeoutMillis: 30000, - connectionTimeoutMillis: 10000, - ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false, - }; + getPool() { + if (!this.pool) { + const config = { + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT) || 5432, + database: process.env.DB_NAME || 'otto', + user: process.env.DB_USER || 'otto_user', + password: process.env.DB_PASSWORD, + max: 20, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 10000, + ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false, + } - logger.info('Database connection config:', { - host: config.host, - port: config.port, - database: config.database, - user: config.user, - password: config.password ? '[SET]' : '[NOT SET]', - ssl: config.ssl ? 'enabled' : 'disabled' - }); + logger.info('Database connection config:', { + host: config.host, + port: config.port, + database: config.database, + user: config.user, + password: config.password ? '[SET]' : '[NOT SET]', + ssl: config.ssl ? 'enabled' : 'disabled' + }) - this.pool = new Pool(config); + this.pool = new Pool(config) - this.pool.on('error', (err) => { - logger.error('Unexpected error on idle client', err); - }); + this.pool.on('error', (err) => { + logger.error('Unexpected error on idle client', err) + }) + } + return this.pool } - return this.pool; - } - async query(text, params) { - const start = Date.now(); - const pool = this.getPool(); - const client = await pool.connect(); + async query(text, params) { + const start = Date.now() + const pool = this.getPool() + const client = await pool.connect() - try { - const res = await client.query(text, params); - const duration = Date.now() - start; - logger.debug('Executed query', { text, duration, rows: res.rowCount }); - return res; - } catch (error) { - logger.error('Database query error', { error: error.message, text }); - throw error; - } finally { - client.release(); + try { + const res = await client.query(text, params) + const duration = Date.now() - start + logger.debug('Executed query', { text, duration, rows: res.rowCount }) + return res + } catch (error) { + logger.error('Database query error', { error: error.message, text }) + throw error + } finally { + client.release() + } } - } - async transaction(callback) { - const pool = this.getPool(); - const client = await pool.connect(); + async transaction(callback) { + const pool = this.getPool() + const client = await pool.connect() - try { - await client.query('BEGIN'); - const result = await callback(client); - await client.query('COMMIT'); - return result; - } catch (error) { - await client.query('ROLLBACK'); - throw error; - } finally { - client.release(); + try { + await client.query('BEGIN') + const result = await callback(client) + await client.query('COMMIT') + return result + } catch (error) { + await client.query('ROLLBACK') + throw error + } finally { + client.release() + } } - } - async testConnection() { - try { - const result = await this.query('SELECT NOW() as current_time'); - logger.info('Database connection test successful', { - time: result.rows[0].current_time - }); - return true; - } catch (error) { - logger.error('Database connection test failed', error); - throw error; + async testConnection() { + try { + const result = await this.query('SELECT NOW() as current_time') + logger.info('Database connection test successful', { + time: result.rows[0].current_time + }) + return true + } catch (error) { + logger.error('Database connection test failed', error) + throw error + } } - } - async close() { - if (this.pool) { - await this.pool.end(); - logger.info('Database connection pool closed'); + async close() { + if (this.pool) { + await this.pool.end() + logger.info('Database connection pool closed') + } } - } } -const database = new Database(); -export default database; +const database = new Database() +export default database diff --git a/src/config/logger.js b/src/config/logger.js index 876b203..4ec15c9 100644 --- a/src/config/logger.js +++ b/src/config/logger.js @@ -1,61 +1,61 @@ -import winston from 'winston'; -import path from 'path'; -import { fileURLToPath } from 'url'; +import winston from 'winston' +import path from 'path' +import { fileURLToPath } from 'url' -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) -const logLevel = process.env.LOG_LEVEL || 'info'; -const logFile = process.env.LOG_FILE || path.join(__dirname, '../../logs/otto.log'); +const logLevel = process.env.LOG_LEVEL || 'info' +const logFile = process.env.LOG_FILE || path.join(__dirname, '../../logs/otto.log') // Custom log format const logFormat = winston.format.combine( - winston.format.timestamp({ - format: 'YYYY-MM-DD HH:mm:ss' - }), - winston.format.errors({ stack: true }), - winston.format.json() -); + winston.format.timestamp({ + format: 'YYYY-MM-DD HH:mm:ss' + }), + winston.format.errors({ stack: true }), + winston.format.json() +) // Console format for development const consoleFormat = winston.format.combine( - winston.format.colorize(), - winston.format.timestamp({ - format: 'HH:mm:ss' - }), - winston.format.printf(({ timestamp, level, message, ...meta }) => { - let log = `${timestamp} [${level}]: ${message}`; - if (Object.keys(meta).length > 0) { - log += ` ${JSON.stringify(meta)}`; - } - return log; - }) -); + winston.format.colorize(), + winston.format.timestamp({ + format: 'HH:mm:ss' + }), + winston.format.printf(({ timestamp, level, message, ...meta }) => { + let log = `${timestamp} [${level}]: ${message}` + if (Object.keys(meta).length > 0) { + log += ` ${JSON.stringify(meta)}` + } + return log + }) +) const logger = winston.createLogger({ - level: logLevel, - format: logFormat, - defaultMeta: { service: 'otto' }, - transports: [ - new winston.transports.File({ - filename: logFile, - maxsize: 5242880, // 5MB - maxFiles: 5, - }), - new winston.transports.File({ - filename: path.join(path.dirname(logFile), 'error.log'), - level: 'error', - maxsize: 5242880, - maxFiles: 5, - }), - ], -}); + level: logLevel, + format: logFormat, + defaultMeta: { service: 'otto' }, + transports: [ + new winston.transports.File({ + filename: logFile, + maxsize: 5242880, // 5MB + maxFiles: 5, + }), + new winston.transports.File({ + filename: path.join(path.dirname(logFile), 'error.log'), + level: 'error', + maxsize: 5242880, + maxFiles: 5, + }), + ], +}) // Add console transport in development if (process.env.NODE_ENV !== 'production') { - logger.add(new winston.transports.Console({ - format: consoleFormat - })); + logger.add(new winston.transports.Console({ + format: consoleFormat + })) } -export default logger; +export default logger diff --git a/src/config/publicContexts.js b/src/config/publicContexts.js index 608857c..044bde8 100644 --- a/src/config/publicContexts.js +++ b/src/config/publicContexts.js @@ -2,19 +2,19 @@ // Files uploaded to these contexts will be public by default export const PUBLIC_CONTEXTS = [ - 'public', - 'avatars', - 'thumbnails', - 'assets', - 'static', - 'media' -]; + 'public', + 'avatars', + 'thumbnails', + 'assets', + 'static', + 'media' +] export const isPublicContext = (context) => { - return PUBLIC_CONTEXTS.includes(context.toLowerCase()); -}; + return PUBLIC_CONTEXTS.includes(context.toLowerCase()) +} export default { - PUBLIC_CONTEXTS, - isPublicContext -}; + PUBLIC_CONTEXTS, + isPublicContext +} diff --git a/src/controllers/ChunkedUploadController.js b/src/controllers/ChunkedUploadController.js index 588f596..4995ddd 100644 --- a/src/controllers/ChunkedUploadController.js +++ b/src/controllers/ChunkedUploadController.js @@ -1,432 +1,433 @@ -import fs from 'fs'; -import ChunkedUploadService from '../services/ChunkedUploadService.js'; -import TokenService from '../services/TokenService.js'; -import { asyncHandler } from '../middleware/errorHandler.js'; -import logger from '../config/logger.js'; +import fs from 'fs' +import ChunkedUploadService from '../services/ChunkedUploadService.js' +// eslint-disable-next-line no-unused-vars +import TokenService from '../services/TokenService.js' +import { asyncHandler } from '../middleware/errorHandler.js' +import logger from '../config/logger.js' class ChunkedUploadController { - /** + /** * Initialize a chunked upload session * POST /api/upload/chunk/init */ - initializeUpload = asyncHandler(async (req, res) => { - const { - originalFilename, - totalSize, - mimeType, - context = 'general', - metadata = {} - } = req.body; - - // Validate required fields - if (!originalFilename || !totalSize || !mimeType) { - return res.status(400).json({ - error: 'originalFilename, totalSize, and mimeType are required', - code: 'MISSING_REQUIRED_FIELDS' - }); - } + initializeUpload = asyncHandler(async (req, res) => { + const { + originalFilename, + totalSize, + mimeType, + context = 'general', + metadata = {} + } = req.body + + // Validate required fields + if (!originalFilename || !totalSize || !mimeType) { + return res.status(400).json({ + error: 'originalFilename, totalSize, and mimeType are required', + code: 'MISSING_REQUIRED_FIELDS' + }) + } - // Validate file size - const maxFileSize = parseInt(process.env.MAX_TOTAL_FILE_SIZE) || 1024 * 1024 * 1024; // 1GB default - if (totalSize > maxFileSize) { - return res.status(400).json({ - error: `File too large. Maximum size is ${Math.round(maxFileSize / 1024 / 1024)}MB`, - code: 'FILE_TOO_LARGE' - }); - } + // Validate file size + const maxFileSize = parseInt(process.env.MAX_TOTAL_FILE_SIZE) || 1024 * 1024 * 1024 // 1GB default + if (totalSize > maxFileSize) { + return res.status(400).json({ + error: `File too large. Maximum size is ${Math.round(maxFileSize / 1024 / 1024)}MB`, + code: 'FILE_TOO_LARGE' + }) + } - // Calculate total chunks needed - const chunkSize = ChunkedUploadService.chunkSize; - const totalChunks = Math.ceil(totalSize / chunkSize); - - // Determine upload context and user from authentication - let uploadContext = context; - let uploadedBy = 'system'; - let uploadSource = 'api'; - - if (req.authenticationType === 'service') { - uploadedBy = req.body.uploadedBy || 'service'; - uploadSource = 'service'; - } else if (req.authenticationType === 'upload_token') { - uploadContext = req.uploadToken.context; - uploadedBy = req.uploadToken.uploadedBy; - uploadSource = 'frontend'; - - // Validate token constraints - if (totalSize > req.uploadToken.maxSize) { - return res.status(400).json({ - error: `File size exceeds token limit of ${req.uploadToken.maxSize} bytes`, - code: 'TOKEN_SIZE_LIMIT_EXCEEDED' - }); - } - - // Check allowed types if specified in token - if (req.uploadToken.allowedTypes && !req.uploadToken.allowedTypes.includes(mimeType)) { - return res.status(400).json({ - error: 'File type not allowed by this token', - code: 'TOKEN_TYPE_NOT_ALLOWED' - }); - } - } else if (req.authenticationType === 'jwt') { - uploadedBy = req.user.sub || req.user.id; - uploadSource = 'user'; - } + // Calculate total chunks needed + const chunkSize = ChunkedUploadService.chunkSize + const totalChunks = Math.ceil(totalSize / chunkSize) + + // Determine upload context and user from authentication + let uploadContext = context + let uploadedBy = 'system' + let uploadSource = 'api' + + if (req.authenticationType === 'service') { + uploadedBy = req.body.uploadedBy || 'service' + uploadSource = 'service' + } else if (req.authenticationType === 'upload_token') { + uploadContext = req.uploadToken.context + uploadedBy = req.uploadToken.uploadedBy + uploadSource = 'frontend' + + // Validate token constraints + if (totalSize > req.uploadToken.maxSize) { + return res.status(400).json({ + error: `File size exceeds token limit of ${req.uploadToken.maxSize} bytes`, + code: 'TOKEN_SIZE_LIMIT_EXCEEDED' + }) + } + + // Check allowed types if specified in token + if (req.uploadToken.allowedTypes && !req.uploadToken.allowedTypes.includes(mimeType)) { + return res.status(400).json({ + error: 'File type not allowed by this token', + code: 'TOKEN_TYPE_NOT_ALLOWED' + }) + } + } else if (req.authenticationType === 'jwt') { + uploadedBy = req.user.sub || req.user.id + uploadSource = 'user' + } - try { - const session = await ChunkedUploadService.initializeSession({ - originalFilename, - totalSize, - totalChunks, - mimeType, - context: uploadContext, - uploadedBy, - uploadSource, - metadata - }); - - logger.info('Chunked upload session initialized', { - sessionId: session.sessionId, - originalFilename, - totalSize, - totalChunks, - uploadedBy - }); - - res.status(201).json({ - success: true, - data: { - sessionId: session.sessionId, - chunkSize: session.chunkSize, - totalChunks, - expiresAt: session.expiresAt + try { + const session = await ChunkedUploadService.initializeSession({ + originalFilename, + totalSize, + totalChunks, + mimeType, + context: uploadContext, + uploadedBy, + uploadSource, + metadata + }) + + logger.info('Chunked upload session initialized', { + sessionId: session.sessionId, + originalFilename, + totalSize, + totalChunks, + uploadedBy + }) + + res.status(201).json({ + success: true, + data: { + sessionId: session.sessionId, + chunkSize: session.chunkSize, + totalChunks, + expiresAt: session.expiresAt + } + }) + + } catch (error) { + logger.error('Failed to initialize chunked upload', { + error: error.message, + originalFilename, + totalSize + }) + + res.status(500).json({ + error: 'Failed to initialize chunked upload', + code: 'INITIALIZATION_FAILED' + }) } - }); - - } catch (error) { - logger.error('Failed to initialize chunked upload', { - error: error.message, - originalFilename, - totalSize - }); - - res.status(500).json({ - error: 'Failed to initialize chunked upload', - code: 'INITIALIZATION_FAILED' - }); - } - }); + }) - /** + /** * Upload a single chunk * POST /api/upload/chunk/:sessionId/:chunkIndex */ - uploadChunk = asyncHandler(async (req, res) => { - const { sessionId, chunkIndex } = req.params; - const chunkIndexNum = parseInt(chunkIndex); - - if (!req.file) { - return res.status(400).json({ - error: 'No chunk data provided', - code: 'NO_CHUNK_DATA' - }); - } + uploadChunk = asyncHandler(async (req, res) => { + const { sessionId, chunkIndex } = req.params + const chunkIndexNum = parseInt(chunkIndex) + + if (!req.file) { + return res.status(400).json({ + error: 'No chunk data provided', + code: 'NO_CHUNK_DATA' + }) + } - if (isNaN(chunkIndexNum) || chunkIndexNum < 0) { - return res.status(400).json({ - error: 'Invalid chunk index', - code: 'INVALID_CHUNK_INDEX' - }); - } try { - // Read chunk data - const chunkBuffer = fs.readFileSync(req.file.path); - const chunkSize = req.file.size; - - // Upload chunk - const result = await ChunkedUploadService.uploadChunk( - sessionId, - chunkIndexNum, - chunkBuffer, - chunkSize - ); - - // Clean up temporary file - fs.unlinkSync(req.file.path); - - logger.info('Chunk uploaded successfully', { - sessionId, - chunkIndex: chunkIndexNum, - chunkSize, - progress: result.progress - }); - - res.json({ - success: true, - data: result - }); } catch (error) { - // Clean up temporary file on error - if (req.file && fs.existsSync(req.file.path)) { - fs.unlinkSync(req.file.path); - } - - logger.error('Chunk upload failed', { - sessionId, - chunkIndex: chunkIndexNum, - error: error.message - }); - - if (error.message.includes('not found') || error.message.includes('expired')) { - return res.status(404).json({ - error: error.message, - code: 'SESSION_NOT_FOUND' - }); - } - - res.status(500).json({ - error: 'Chunk upload failed', - code: 'CHUNK_UPLOAD_FAILED', - details: error.message - }); - } - }); + if (isNaN(chunkIndexNum) || chunkIndexNum < 0) { + return res.status(400).json({ + error: 'Invalid chunk index', + code: 'INVALID_CHUNK_INDEX' + }) + } try { + // Read chunk data + const chunkBuffer = fs.readFileSync(req.file.path) + const chunkSize = req.file.size + + // Upload chunk + const result = await ChunkedUploadService.uploadChunk( + sessionId, + chunkIndexNum, + chunkBuffer, + chunkSize + ) + + // Clean up temporary file + fs.unlinkSync(req.file.path) + + logger.info('Chunk uploaded successfully', { + sessionId, + chunkIndex: chunkIndexNum, + chunkSize, + progress: result.progress + }) + + res.json({ + success: true, + data: result + }) } catch (error) { + // Clean up temporary file on error + if (req.file && fs.existsSync(req.file.path)) { + fs.unlinkSync(req.file.path) + } + + logger.error('Chunk upload failed', { + sessionId, + chunkIndex: chunkIndexNum, + error: error.message + }) + + if (error.message.includes('not found') || error.message.includes('expired')) { + return res.status(404).json({ + error: error.message, + code: 'SESSION_NOT_FOUND' + }) + } + + res.status(500).json({ + error: 'Chunk upload failed', + code: 'CHUNK_UPLOAD_FAILED', + details: error.message + }) + } + }) - /** + /** * Get upload session status * GET /api/upload/chunk/:sessionId/status */ - getSessionStatus = asyncHandler(async (req, res) => { - const { sessionId } = req.params; - - try { - const status = ChunkedUploadService.getSessionStatus(sessionId); - - if (!status) { - return res.status(404).json({ - error: 'Upload session not found or expired', - code: 'SESSION_NOT_FOUND' - }); - } - - res.json({ - success: true, - data: status - }); - - } catch (error) { - logger.error('Failed to get session status', { - sessionId, - error: error.message - }); - - res.status(500).json({ - error: 'Failed to get session status', - code: 'STATUS_ERROR' - }); - } - }); + getSessionStatus = asyncHandler(async (req, res) => { + const { sessionId } = req.params + + try { + const status = ChunkedUploadService.getSessionStatus(sessionId) + + if (!status) { + return res.status(404).json({ + error: 'Upload session not found or expired', + code: 'SESSION_NOT_FOUND' + }) + } + + res.json({ + success: true, + data: status + }) + + } catch (error) { + logger.error('Failed to get session status', { + sessionId, + error: error.message + }) + + res.status(500).json({ + error: 'Failed to get session status', + code: 'STATUS_ERROR' + }) + } + }) - /** + /** * Complete chunked upload (assemble file) * POST /api/upload/chunk/:sessionId/complete */ completeUpload = asyncHandler(async (req, res) => { - const { sessionId } = req.params; - - try { - // First check session status to see if all chunks are uploaded - const status = ChunkedUploadService.getSessionStatus(sessionId); - if (!status) { - return res.status(404).json({ - error: 'Upload session not found', - code: 'SESSION_NOT_FOUND' - }); - } - - logger.debug('Completion request received', { - sessionId, - uploadedChunks: status.uploadedChunks, - totalChunks: status.totalChunks, - missingChunks: status.missingChunks, - completed: status.completed - }); - - // If there are missing chunks, return error with details - if (status.missingChunks.length > 0) { - return res.status(400).json({ - error: `Missing chunks: ${status.missingChunks.join(', ')}`, - code: 'MISSING_CHUNKS', - missingChunks: status.missingChunks - }); - } - - const processedFile = await ChunkedUploadService.assembleFile(sessionId); - - // Debug logging to see what we got back - logger.debug('Assembled file object:', { - sessionId, - processedFile: processedFile ? { - id: processedFile.id, - file_hash: processedFile.file_hash, - original_name: processedFile.original_name, - hasFileHash: !!processedFile.file_hash, - keys: Object.keys(processedFile || {}) - } : 'null' - }); - - if (!processedFile) { - throw new Error('No file was returned from assembleFile'); - } - - if (!processedFile.file_hash) { - throw new Error('Assembled file is missing file_hash property'); - } - - // Format response similar to regular upload - const hashPrefix = processedFile.file_hash.substring(0, 12); - const fileExt = processedFile.original_name.split('.').pop().toLowerCase(); - - const fileResponse = { - id: processedFile.id, - filename: processedFile.filename, - originalName: processedFile.original_name, - mimeType: processedFile.mime_type, - fileSize: processedFile.file_size, - uploadContext: processedFile.upload_context, - uploadedAt: processedFile.created_at, - isPublic: processedFile.is_public, - url: `/files/${processedFile.id}`, - publicUrl: processedFile.is_public ? `/public/${processedFile.upload_context}/${hashPrefix}` : null, - publicUrlWithExt: processedFile.is_public ? `/public/${processedFile.upload_context}/${hashPrefix}.${fileExt}` : null, - shortPublicUrl: processedFile.is_public ? `/p/${processedFile.upload_context}/${hashPrefix}` : null, - shortPublicUrlWithExt: processedFile.is_public ? `/p/${processedFile.upload_context}/${hashPrefix}.${fileExt}` : null, - legacyPublicUrl: processedFile.is_public ? `/public/${processedFile.upload_context}/${processedFile.original_name}` : null - }; - - logger.info('Chunked upload completed successfully', { - sessionId, - fileId: processedFile.id, - originalName: processedFile.original_name, - fileSize: processedFile.file_size - }); - - res.json({ - success: true, - data: { - file: fileResponse, - sessionId, - chunkedUpload: true + const { sessionId } = req.params + + try { + // First check session status to see if all chunks are uploaded + const status = ChunkedUploadService.getSessionStatus(sessionId) + if (!status) { + return res.status(404).json({ + error: 'Upload session not found', + code: 'SESSION_NOT_FOUND' + }) + } + + logger.debug('Completion request received', { + sessionId, + uploadedChunks: status.uploadedChunks, + totalChunks: status.totalChunks, + missingChunks: status.missingChunks, + completed: status.completed + }) + + // If there are missing chunks, return error with details + if (status.missingChunks.length > 0) { + return res.status(400).json({ + error: `Missing chunks: ${status.missingChunks.join(', ')}`, + code: 'MISSING_CHUNKS', + missingChunks: status.missingChunks + }) + } + + const processedFile = await ChunkedUploadService.assembleFile(sessionId) + + // Debug logging to see what we got back + logger.debug('Assembled file object:', { + sessionId, + processedFile: processedFile ? { + id: processedFile.id, + file_hash: processedFile.file_hash, + original_name: processedFile.original_name, + hasFileHash: !!processedFile.file_hash, + keys: Object.keys(processedFile || {}) + } : 'null' + }) + + if (!processedFile) { + throw new Error('No file was returned from assembleFile') + } + + if (!processedFile.file_hash) { + throw new Error('Assembled file is missing file_hash property') + } + + // Format response similar to regular upload + const hashPrefix = processedFile.file_hash.substring(0, 12) + const fileExt = processedFile.original_name.split('.').pop().toLowerCase() + + const fileResponse = { + id: processedFile.id, + filename: processedFile.filename, + originalName: processedFile.original_name, + mimeType: processedFile.mime_type, + fileSize: processedFile.file_size, + uploadContext: processedFile.upload_context, + uploadedAt: processedFile.created_at, + isPublic: processedFile.is_public, + url: `/files/${processedFile.id}`, + publicUrl: processedFile.is_public ? `/public/${processedFile.upload_context}/${hashPrefix}` : null, + publicUrlWithExt: processedFile.is_public ? `/public/${processedFile.upload_context}/${hashPrefix}.${fileExt}` : null, + shortPublicUrl: processedFile.is_public ? `/p/${processedFile.upload_context}/${hashPrefix}` : null, + shortPublicUrlWithExt: processedFile.is_public ? `/p/${processedFile.upload_context}/${hashPrefix}.${fileExt}` : null, + legacyPublicUrl: processedFile.is_public ? `/public/${processedFile.upload_context}/${processedFile.original_name}` : null + } + + logger.info('Chunked upload completed successfully', { + sessionId, + fileId: processedFile.id, + originalName: processedFile.original_name, + fileSize: processedFile.file_size + }) + + res.json({ + success: true, + data: { + file: fileResponse, + sessionId, + chunkedUpload: true + } + }) + + } catch (error) { + logger.error('Failed to complete chunked upload', { + sessionId, + error: error.message, + stack: error.stack + }) + + if (error.message.includes('not found')) { + return res.status(404).json({ + error: 'Upload session not found', + code: 'SESSION_NOT_FOUND' + }) + } + + if (error.message.includes('Missing chunk')) { + return res.status(400).json({ + error: error.message, + code: 'MISSING_CHUNKS' + }) + } + + res.status(500).json({ + error: 'Failed to complete upload', + code: 'COMPLETION_FAILED', + details: error.message + }) } - }); - - } catch (error) { - logger.error('Failed to complete chunked upload', { - sessionId, - error: error.message, - stack: error.stack - }); - - if (error.message.includes('not found')) { - return res.status(404).json({ - error: 'Upload session not found', - code: 'SESSION_NOT_FOUND' - }); - } - - if (error.message.includes('Missing chunk')) { - return res.status(400).json({ - error: error.message, - code: 'MISSING_CHUNKS' - }); - } - - res.status(500).json({ - error: 'Failed to complete upload', - code: 'COMPLETION_FAILED', - details: error.message - }); - } - }); + }) - /** + /** * Cancel upload session * DELETE /api/upload/chunk/:sessionId */ - cancelUpload = asyncHandler(async (req, res) => { - const { sessionId } = req.params; - - try { - const cancelled = await ChunkedUploadService.cancelSession(sessionId); - - if (!cancelled) { - return res.status(404).json({ - error: 'Upload session not found', - code: 'SESSION_NOT_FOUND' - }); - } - - logger.info('Upload session cancelled', { sessionId }); - - res.json({ - success: true, - data: { - sessionId, - cancelled: true + cancelUpload = asyncHandler(async (req, res) => { + const { sessionId } = req.params + + try { + const cancelled = await ChunkedUploadService.cancelSession(sessionId) + + if (!cancelled) { + return res.status(404).json({ + error: 'Upload session not found', + code: 'SESSION_NOT_FOUND' + }) + } + + logger.info('Upload session cancelled', { sessionId }) + + res.json({ + success: true, + data: { + sessionId, + cancelled: true + } + }) + + } catch (error) { + logger.error('Failed to cancel upload session', { + sessionId, + error: error.message + }) + + res.status(500).json({ + error: 'Failed to cancel upload session', + code: 'CANCELLATION_FAILED' + }) } - }); - - } catch (error) { - logger.error('Failed to cancel upload session', { - sessionId, - error: error.message - }); - - res.status(500).json({ - error: 'Failed to cancel upload session', - code: 'CANCELLATION_FAILED' - }); - } - }); + }) - /** + /** * Get chunked upload configuration * GET /api/upload/chunk/config */ - getConfig = asyncHandler(async (req, res) => { - const config = ChunkedUploadService.getConfig(); - - res.json({ - success: true, - data: { - chunkSize: config.chunkSize, - maxConcurrentChunks: config.maxConcurrentChunks, - sessionTimeout: config.sessionTimeout, - formattedChunkSize: this.formatBytes(config.chunkSize), - formattedSessionTimeout: this.formatDuration(config.sessionTimeout) - } - }); - }); - - /** + getConfig = asyncHandler(async (req, res) => { + const config = ChunkedUploadService.getConfig() + + res.json({ + success: true, + data: { + chunkSize: config.chunkSize, + maxConcurrentChunks: config.maxConcurrentChunks, + sessionTimeout: config.sessionTimeout, + formattedChunkSize: this.formatBytes(config.chunkSize), + formattedSessionTimeout: this.formatDuration(config.sessionTimeout) + } + }) + }) + + /** * Format bytes to human readable format */ - formatBytes(bytes) { - if (bytes === 0) return '0 Bytes'; - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; - } - - /** + formatBytes(bytes) { + if (bytes === 0) return '0 Bytes' + const k = 1024 + const sizes = ['Bytes', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] + } + + /** * Format duration to human readable format */ - formatDuration(ms) { - const hours = Math.floor(ms / (1000 * 60 * 60)); - const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60)); + formatDuration(ms) { + const hours = Math.floor(ms / (1000 * 60 * 60)) + const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60)) - if (hours > 0) { - return `${hours}h ${minutes}m`; + if (hours > 0) { + return `${hours}h ${minutes}m` + } + return `${minutes}m` } - return `${minutes}m`; - } } -export default new ChunkedUploadController(); +export default new ChunkedUploadController() diff --git a/src/controllers/FileController.js b/src/controllers/FileController.js index 2ce50df..7cac683 100644 --- a/src/controllers/FileController.js +++ b/src/controllers/FileController.js @@ -1,720 +1,723 @@ -import path from 'path'; -import fs from 'fs'; -import FileService from '../services/FileService.js'; -import TokenService from '../services/TokenService.js'; -import { asyncHandler } from '../middleware/errorHandler.js'; -import logger from '../config/logger.js'; +import path from 'path' +import fs from 'fs' +import FileService from '../services/FileService.js' +import TokenService from '../services/TokenService.js' +import { asyncHandler } from '../middleware/errorHandler.js' +import logger from '../config/logger.js' class FileController { - serveFile = asyncHandler(async (req, res) => { - const { fileId } = req.params; - const { token, download, thumbnail } = req.query; - - // Check for token-based access - if (token) { - try { - const decoded = TokenService.verifyToken(token); - if (decoded.type !== 'file_access' || decoded.fileId !== fileId) { - return res.status(401).json({ - error: 'Invalid file access token', - code: 'INVALID_FILE_TOKEN' - }); + serveFile = asyncHandler(async (req, res) => { + const { fileId } = req.params + const { token, download, thumbnail } = req.query + + // Check for token-based access + if (token) { + try { + const decoded = TokenService.verifyToken(token) + if (decoded.type !== 'file_access' || decoded.fileId !== fileId) { + return res.status(401).json({ + error: 'Invalid file access token', + code: 'INVALID_FILE_TOKEN' + }) + } + } catch (error) { + logger.warn('Invalid file access token', { fileId, error: error.message }) + return res.status(401).json({ + error: 'Invalid or expired file access token', + code: 'INVALID_FILE_TOKEN' + }) + } + } else { + // Require authentication for non-token access + if (!req.serviceAuthenticated && !req.user && !req.uploadToken) { + return res.status(401).json({ + error: 'Authentication required', + code: 'AUTHENTICATION_REQUIRED' + }) + } } - } catch (error) { - return res.status(401).json({ - error: 'Invalid or expired file access token', - code: 'INVALID_FILE_TOKEN' - }); - } - } else { - // Require authentication for non-token access - if (!req.serviceAuthenticated && !req.user && !req.uploadToken) { - return res.status(401).json({ - error: 'Authentication required', - code: 'AUTHENTICATION_REQUIRED' - }); - } - } - // Get file record - const file = await FileService.getFile(fileId, { trackAccess: !token }); + // Get file record + const file = await FileService.getFile(fileId, { trackAccess: !token }) - if (!file) { - return res.status(404).json({ - error: 'File not found', - code: 'FILE_NOT_FOUND' - }); - } - - // Check access permissions (skip for token-based access) - if (!token && !FileService.validateFileAccess(file, req)) { - return res.status(403).json({ - error: 'Access denied', - code: 'ACCESS_DENIED' - }); - } - - try { - let filePath = file.file_path; - let mimeType = file.mime_type; // Serve thumbnail if requested - if (thumbnail === 'true') { - const thumbnailPath = FileService.getThumbnailPath(file.file_path, fileId); - const fs = await import('fs'); - if (fs.existsSync(thumbnailPath)) { - filePath = thumbnailPath; - mimeType = 'image/webp'; + if (!file) { + return res.status(404).json({ + error: 'File not found', + code: 'FILE_NOT_FOUND' + }) } - } - const fileStream = FileService.getFileStream(filePath); - const fs = await import('fs'); - - // Set appropriate headers - res.set({ - 'Content-Type': mimeType, - 'Content-Length': fs.statSync(filePath).size, - 'Cache-Control': 'private, max-age=3600', - 'X-File-ID': fileId - }); - - // Force download if requested - if (download === 'true') { - res.set('Content-Disposition', `attachment; filename="${file.original_name}"`); - } else { - res.set('Content-Disposition', `inline; filename="${file.original_name}"`); - } - - // Pipe file stream to response - fileStream.pipe(res); - - fileStream.on('error', (error) => { - logger.error('File stream error', { fileId, error: error.message }); - if (!res.headersSent) { - res.status(500).json({ - error: 'Failed to serve file', - code: 'FILE_STREAM_ERROR' - }); + // Check access permissions (skip for token-based access) + if (!token && !FileService.validateFileAccess(file, req)) { + return res.status(403).json({ + error: 'Access denied', + code: 'ACCESS_DENIED' + }) } - }); - } catch (error) { - logger.error('Failed to serve file', { fileId, error: error.message }); + try { + let filePath = file.file_path + let mimeType = file.mime_type // Serve thumbnail if requested + if (thumbnail === 'true') { + const thumbnailPath = FileService.getThumbnailPath(file.file_path, fileId) + const fs = await import('fs') + if (fs.existsSync(thumbnailPath)) { + filePath = thumbnailPath + mimeType = 'image/webp' + } + } + + const fileStream = FileService.getFileStream(filePath) + const fs = await import('fs') - if (error.message === 'File not found on disk') { - return res.status(404).json({ - error: 'File not found on disk', - code: 'FILE_NOT_FOUND_ON_DISK' - }); - } - - throw error; - } - }); + // Set appropriate headers + res.set({ + 'Content-Type': mimeType, + 'Content-Length': fs.statSync(filePath).size, + 'Cache-Control': 'private, max-age=3600', + 'X-File-ID': fileId + }) + + // Force download if requested + if (download === 'true') { + res.set('Content-Disposition', `attachment; filename="${file.original_name}"`) + } else { + res.set('Content-Disposition', `inline; filename="${file.original_name}"`) + } + + // Pipe file stream to response + fileStream.pipe(res) + + fileStream.on('error', (error) => { + logger.error('File stream error', { fileId, error: error.message }) + if (!res.headersSent) { + res.status(500).json({ + error: 'Failed to serve file', + code: 'FILE_STREAM_ERROR' + }) + } + }) + + } catch (error) { + logger.error('Failed to serve file', { fileId, error: error.message }) + + if (error.message === 'File not found on disk') { + return res.status(404).json({ + error: 'File not found on disk', + code: 'FILE_NOT_FOUND_ON_DISK' + }) + } + + throw error + } + }) - /** + /** * Get file information */ - getFileInfo = asyncHandler(async (req, res) => { - const { fileId } = req.params; + getFileInfo = asyncHandler(async (req, res) => { + const { fileId } = req.params - const file = await FileService.getFile(fileId, { trackAccess: false }); + const file = await FileService.getFile(fileId, { trackAccess: false }) - if (!file) { - return res.status(404).json({ - error: 'File not found', - code: 'FILE_NOT_FOUND' - }); - } + if (!file) { + return res.status(404).json({ + error: 'File not found', + code: 'FILE_NOT_FOUND' + }) + } - // Check access permissions - if (!FileService.validateFileAccess(file, req)) { - return res.status(403).json({ - error: 'Access denied', - code: 'ACCESS_DENIED' - }); - } res.json({ - success: true, - data: { - id: file.id, - filename: file.filename, - originalName: file.original_name, - mimeType: file.mime_type, - fileSize: file.file_size, - uploadContext: file.upload_context, - uploadedBy: file.uploaded_by, - uploadSource: file.upload_source, - uploadedAt: file.created_at, - lastAccessedAt: file.last_accessed_at, - accessCount: file.access_count, - metadata: file.metadata, - url: `/files/${file.id}`, - downloadUrl: `/files/${file.id}?download=true`, - thumbnailUrl: file.metadata?.hasThumbnail ? `/files/${file.id}?thumbnail=true` : null, - publicUrl: file.is_public ? `/public/${file.upload_context}/${file.file_hash.substring(0, 12)}` : null, - publicUrlWithExt: file.is_public ? (() => { - const hashPrefix = file.file_hash.substring(0, 12); - const fileExt = file.original_name.split('.').pop().toLowerCase(); - return `/public/${file.upload_context}/${hashPrefix}.${fileExt}`; - })() : null, - shortPublicUrl: file.is_public ? `/p/${file.upload_context}/${file.file_hash.substring(0, 12)}` : null, - shortPublicUrlWithExt: file.is_public ? (() => { - const hashPrefix = file.file_hash.substring(0, 12); - const fileExt = file.original_name.split('.').pop().toLowerCase(); - return `/p/${file.upload_context}/${hashPrefix}.${fileExt}`; - })() : null, - legacyPublicUrl: file.is_public ? `/public/${file.upload_context}/${file.original_name}` : null - } - }); - }); - - /** + // Check access permissions + if (!FileService.validateFileAccess(file, req)) { + return res.status(403).json({ + error: 'Access denied', + code: 'ACCESS_DENIED' + }) + } res.json({ + success: true, + data: { + id: file.id, + filename: file.filename, + originalName: file.original_name, + mimeType: file.mime_type, + fileSize: file.file_size, + uploadContext: file.upload_context, + uploadedBy: file.uploaded_by, + uploadSource: file.upload_source, + uploadedAt: file.created_at, + lastAccessedAt: file.last_accessed_at, + accessCount: file.access_count, + metadata: file.metadata, + url: `/files/${file.id}`, + downloadUrl: `/files/${file.id}?download=true`, + thumbnailUrl: file.metadata?.hasThumbnail ? `/files/${file.id}?thumbnail=true` : null, + publicUrl: file.is_public ? `/public/${file.upload_context}/${file.file_hash.substring(0, 12)}` : null, + publicUrlWithExt: file.is_public ? (() => { + const hashPrefix = file.file_hash.substring(0, 12) + const fileExt = file.original_name.split('.').pop().toLowerCase() + return `/public/${file.upload_context}/${hashPrefix}.${fileExt}` + })() : null, + shortPublicUrl: file.is_public ? `/p/${file.upload_context}/${file.file_hash.substring(0, 12)}` : null, + shortPublicUrlWithExt: file.is_public ? (() => { + const hashPrefix = file.file_hash.substring(0, 12) + const fileExt = file.original_name.split('.').pop().toLowerCase() + return `/p/${file.upload_context}/${hashPrefix}.${fileExt}` + })() : null, + legacyPublicUrl: file.is_public ? `/public/${file.upload_context}/${file.original_name}` : null + } + }) + }) + + /** * Delete file */ - deleteFile = asyncHandler(async (req, res) => { - const { fileId } = req.params; + deleteFile = asyncHandler(async (req, res) => { + const { fileId } = req.params - const file = await FileService.getFile(fileId, { trackAccess: false }); + const file = await FileService.getFile(fileId, { trackAccess: false }) - if (!file) { - return res.status(404).json({ - error: 'File not found', - code: 'FILE_NOT_FOUND' - }); - } + if (!file) { + return res.status(404).json({ + error: 'File not found', + code: 'FILE_NOT_FOUND' + }) + } - // Check permissions - only allow deletion by uploader or service - if (!req.serviceAuthenticated && + // Check permissions - only allow deletion by uploader or service + if (!req.serviceAuthenticated && (!req.user || file.uploaded_by !== req.user.id)) { - return res.status(403).json({ - error: 'Access denied - can only delete own files', - code: 'DELETE_ACCESS_DENIED' - }); - } + return res.status(403).json({ + error: 'Access denied - can only delete own files', + code: 'DELETE_ACCESS_DENIED' + }) + } - const deleted = await FileService.deleteFile(fileId); + const deleted = await FileService.deleteFile(fileId) - if (!deleted) { - return res.status(404).json({ - error: 'File not found', - code: 'FILE_NOT_FOUND' - }); - } + if (!deleted) { + return res.status(404).json({ + error: 'File not found', + code: 'FILE_NOT_FOUND' + }) + } - logger.info('File deleted via API', { - fileId, - deletedBy: req.user?.id || 'service' - }); + logger.info('File deleted via API', { + fileId, + deletedBy: req.user?.id || 'service' + }) - res.json({ - success: true, - message: 'File deleted successfully' - }); - }); + res.json({ + success: true, + message: 'File deleted successfully' + }) + }) - /** + /** * Generate signed URL for temporary file access */ - generateSignedUrl = asyncHandler(async (req, res) => { - const { fileId } = req.params; - const { expiresIn = 3600 } = req.body; + generateSignedUrl = asyncHandler(async (req, res) => { + const { fileId } = req.params + const { expiresIn = 3600 } = req.body - const file = await FileService.getFile(fileId, { trackAccess: false }); + const file = await FileService.getFile(fileId, { trackAccess: false }) - if (!file) { - return res.status(404).json({ - error: 'File not found', - code: 'FILE_NOT_FOUND' - }); - } + if (!file) { + return res.status(404).json({ + error: 'File not found', + code: 'FILE_NOT_FOUND' + }) + } - // Check access permissions - if (!FileService.validateFileAccess(file, req)) { - return res.status(403).json({ - error: 'Access denied', - code: 'ACCESS_DENIED' - }); - } + // Check access permissions + if (!FileService.validateFileAccess(file, req)) { + return res.status(403).json({ + error: 'Access denied', + code: 'ACCESS_DENIED' + }) + } - const signedUrl = TokenService.generateSignedUrl(fileId, parseInt(expiresIn)); - - logger.info('Signed URL generated', { - fileId, - expiresIn, - requestedBy: req.user?.id || 'service' - }); - - res.json({ - success: true, - data: { - fileId, - signedUrl: `${req.protocol}://${req.get('host')}${signedUrl}`, - expiresIn: parseInt(expiresIn), - expiresAt: new Date(Date.now() + parseInt(expiresIn) * 1000).toISOString() - } - }); - }); - /** + const signedUrl = TokenService.generateSignedUrl(fileId, parseInt(expiresIn)) + + logger.info('Signed URL generated', { + fileId, + expiresIn, + requestedBy: req.user?.id || 'service' + }) + + res.json({ + success: true, + data: { + fileId, + signedUrl: `${req.protocol}://${req.get('host')}${signedUrl}`, + expiresIn: parseInt(expiresIn), + expiresAt: new Date(Date.now() + parseInt(expiresIn) * 1000).toISOString() + } + }) + }) + /** * Get files by uploader */ - getFilesByUploader = asyncHandler(async (req, res) => { - const { uploaderId } = req.params; - const { limit = 50, offset = 0 } = req.query; - - // Only allow users to see their own files unless it's a service request - if (!req.serviceAuthenticated && req.user?.id !== uploaderId) { - return res.status(403).json({ - error: 'Access denied - can only view own files', - code: 'ACCESS_DENIED' - }); - } + getFilesByUploader = asyncHandler(async (req, res) => { + const { uploaderId } = req.params + const { limit = 50, offset = 0 } = req.query + + // Only allow users to see their own files unless it's a service request + if (!req.serviceAuthenticated && req.user?.id !== uploaderId) { + return res.status(403).json({ + error: 'Access denied - can only view own files', + code: 'ACCESS_DENIED' + }) + } - const files = await FileService.getFilesByUploader(uploaderId, { - limit: parseInt(limit), - offset: parseInt(offset) - }); res.json({ - success: true, - data: { files: files.map(file => { - const hashPrefix = file.file_hash.substring(0, 12); - const fileExt = file.original_name.split('.').pop().toLowerCase(); + const files = await FileService.getFilesByUploader(uploaderId, { + limit: parseInt(limit), + offset: parseInt(offset) + }); res.json({ + success: true, + data: { files: files.map(file => { + const hashPrefix = file.file_hash.substring(0, 12) + const fileExt = file.original_name.split('.').pop().toLowerCase() - return { - id: file.id, - filename: file.filename, - originalName: file.original_name, - mimeType: file.mime_type, - fileSize: file.file_size, - uploadContext: file.upload_context, - uploadedAt: file.created_at, - accessCount: file.access_count, - url: `/files/${file.id}`, - publicUrl: file.is_public ? `/public/${file.upload_context}/${hashPrefix}` : null, - publicUrlWithExt: file.is_public ? `/public/${file.upload_context}/${hashPrefix}.${fileExt}` : null, - shortPublicUrl: file.is_public ? `/p/${file.upload_context}/${hashPrefix}` : null, - shortPublicUrlWithExt: file.is_public ? `/p/${file.upload_context}/${hashPrefix}.${fileExt}` : null, - legacyPublicUrl: file.is_public ? `/public/${file.upload_context}/${file.original_name}` : null - }; - }), - uploaderId, - count: files.length, - pagination: { - limit: parseInt(limit), - offset: parseInt(offset), - hasMore: files.length === parseInt(limit) - } - } - }); - }); - /** + return { + id: file.id, + filename: file.filename, + originalName: file.original_name, + mimeType: file.mime_type, + fileSize: file.file_size, + uploadContext: file.upload_context, + uploadedAt: file.created_at, + accessCount: file.access_count, + url: `/files/${file.id}`, + publicUrl: file.is_public ? `/public/${file.upload_context}/${hashPrefix}` : null, + publicUrlWithExt: file.is_public ? `/public/${file.upload_context}/${hashPrefix}.${fileExt}` : null, + shortPublicUrl: file.is_public ? `/p/${file.upload_context}/${hashPrefix}` : null, + shortPublicUrlWithExt: file.is_public ? `/p/${file.upload_context}/${hashPrefix}.${fileExt}` : null, + legacyPublicUrl: file.is_public ? `/public/${file.upload_context}/${file.original_name}` : null + } + }), + uploaderId, + count: files.length, + pagination: { + limit: parseInt(limit), + offset: parseInt(offset), + hasMore: files.length === parseInt(limit) + } + } + }) + }) + /** * Serve public file by context and filename * No authentication required for public files */ - servePublicFile = asyncHandler(async (req, res) => { - const { context, filename } = req.params; - const { download, thumbnail } = req.query; + servePublicFile = asyncHandler(async (req, res) => { + const { context, filename } = req.params + const { download, thumbnail } = req.query - // Get public file record - const file = await FileService.getPublicFileByContextAndFilename(context, filename); + // Get public file record + const file = await FileService.getPublicFileByContextAndFilename(context, filename) - if (!file) { - return res.status(404).json({ - error: 'Public file not found', - code: 'PUBLIC_FILE_NOT_FOUND' - }); - } + if (!file) { + return res.status(404).json({ + error: 'Public file not found', + code: 'PUBLIC_FILE_NOT_FOUND' + }) + } - try { - let filePath = file.file_path; - let mimeType = file.mime_type; + try { + let filePath = file.file_path + let mimeType = file.mime_type - // Serve thumbnail if requested - if (thumbnail === 'true') { - const thumbnailPath = FileService.getThumbnailPath(file.file_path, file.id); - if (fs.existsSync(thumbnailPath)) { - filePath = thumbnailPath; - mimeType = 'image/webp'; - } - } + // Serve thumbnail if requested + if (thumbnail === 'true') { + const thumbnailPath = FileService.getThumbnailPath(file.file_path, file.id) + if (fs.existsSync(thumbnailPath)) { + filePath = thumbnailPath + mimeType = 'image/webp' + } + } - const fileStream = FileService.getFileStream(filePath); + const fileStream = FileService.getFileStream(filePath) - // Set appropriate headers for public files - res.set({ - 'Content-Type': mimeType, - 'Content-Length': fs.statSync(filePath).size, - 'Cache-Control': 'public, max-age=86400', // 24 hours cache for public files - 'X-File-ID': file.id, - 'X-Public-File': 'true' - }); - - // Force download if requested - if (download === 'true') { - res.set('Content-Disposition', `attachment; filename="${file.original_name}"`); - } else { - res.set('Content-Disposition', `inline; filename="${file.original_name}"`); - } - - // Pipe file stream to response - fileStream.pipe(res); - - fileStream.on('error', (error) => { - logger.error('Public file stream error', { - fileId: file.id, - context, - filename, - error: error.message - }); - if (!res.headersSent) { - res.status(500).json({ - error: 'Failed to serve public file', - code: 'PUBLIC_FILE_STREAM_ERROR' - }); - } - }); - - } catch (error) { - logger.error('Failed to serve public file', { - context, - filename, - error: error.message - }); + // Set appropriate headers for public files + res.set({ + 'Content-Type': mimeType, + 'Content-Length': fs.statSync(filePath).size, + 'Cache-Control': 'public, max-age=86400', // 24 hours cache for public files + 'X-File-ID': file.id, + 'X-Public-File': 'true' + }) + + // Force download if requested + if (download === 'true') { + res.set('Content-Disposition', `attachment; filename="${file.original_name}"`) + } else { + res.set('Content-Disposition', `inline; filename="${file.original_name}"`) + } + + // Pipe file stream to response + fileStream.pipe(res) + + fileStream.on('error', (error) => { + logger.error('Public file stream error', { + fileId: file.id, + context, + filename, + error: error.message + }) + if (!res.headersSent) { + res.status(500).json({ + error: 'Failed to serve public file', + code: 'PUBLIC_FILE_STREAM_ERROR' + }) + } + }) + + } catch (error) { + logger.error('Failed to serve public file', { + context, + filename, + error: error.message + }) - if (error.message === 'File not found on disk') { - return res.status(404).json({ - error: 'Public file not found on disk', - code: 'PUBLIC_FILE_NOT_FOUND_ON_DISK' - }); - } - - throw error; - } - }); /** + if (error.message === 'File not found on disk') { + return res.status(404).json({ + error: 'Public file not found on disk', + code: 'PUBLIC_FILE_NOT_FOUND_ON_DISK' + }) + } + + throw error + } + }) /** * Serve public file by context and hash (without extension) * Content-addressable URLs prevent collisions * No authentication required for public files */ - servePublicFileByHash = asyncHandler(async (req, res) => { - const { context, hash } = req.params; - const { download, thumbnail } = req.query; + servePublicFileByHash = asyncHandler(async (req, res) => { + const { context, hash } = req.params + // eslint-disable-next-line no-unused-vars + const { download, thumbnail } = req.query - // Get public file record by hash (without filename) - const file = await FileService.getPublicFileByHash(hash, context); + // Get public file record by hash (without filename) + const file = await FileService.getPublicFileByHash(hash, context) - if (!file) { - return res.status(404).json({ - error: 'Public file not found', - code: 'PUBLIC_FILE_NOT_FOUND' - }); - } + if (!file) { + return res.status(404).json({ + error: 'Public file not found', + code: 'PUBLIC_FILE_NOT_FOUND' + }) + } - this.servePublicFileResponse(file, req, res, { context, hash }); - }); + this.servePublicFileResponse(file, req, res, { context, hash }) + }) - /** + /** * Serve public file by context and hash with extension * Content-addressable URLs prevent collisions * No authentication required for public files */ - servePublicFileByHashWithExt = asyncHandler(async (req, res) => { - const { context, hash, ext } = req.params; - const { download, thumbnail } = req.query; + servePublicFileByHashWithExt = asyncHandler(async (req, res) => { + const { context, hash, ext } = req.params + // eslint-disable-next-line no-unused-vars + const { download, thumbnail } = req.query - // Get public file record by hash and verify extension matches - const file = await FileService.getPublicFileByHash(hash, context); + // Get public file record by hash and verify extension matches + const file = await FileService.getPublicFileByHash(hash, context) - if (!file) { - return res.status(404).json({ - error: 'Public file not found', - code: 'PUBLIC_FILE_NOT_FOUND' - }); - } + if (!file) { + return res.status(404).json({ + error: 'Public file not found', + code: 'PUBLIC_FILE_NOT_FOUND' + }) + } - // Verify the extension matches the file's actual extension - const actualExt = path.extname(file.original_name).substring(1).toLowerCase(); - if (ext.toLowerCase() !== actualExt) { - return res.status(404).json({ - error: 'File extension does not match', - code: 'EXTENSION_MISMATCH' - }); - } + // Verify the extension matches the file's actual extension + const actualExt = path.extname(file.original_name).substring(1).toLowerCase() + if (ext.toLowerCase() !== actualExt) { + return res.status(404).json({ + error: 'File extension does not match', + code: 'EXTENSION_MISMATCH' + }) + } - this.servePublicFileResponse(file, req, res, { context, hash, ext }); - }); + this.servePublicFileResponse(file, req, res, { context, hash, ext }) + }) - /** + /** * Common response handler for public files */ - servePublicFileResponse = (file, req, res, routeParams) => { - const { download, thumbnail } = req.query; - - try { - let filePath = file.file_path; - let mimeType = file.mime_type; - - // Serve thumbnail if requested - if (thumbnail === 'true') { - const thumbnailPath = FileService.getThumbnailPath(file.file_path, file.id); - if (fs.existsSync(thumbnailPath)) { - filePath = thumbnailPath; - mimeType = 'image/webp'; - } - } - - const fileStream = FileService.getFileStream(filePath); + servePublicFileResponse = (file, req, res, routeParams) => { + const { download, thumbnail } = req.query + + try { + let filePath = file.file_path + let mimeType = file.mime_type + + // Serve thumbnail if requested + if (thumbnail === 'true') { + const thumbnailPath = FileService.getThumbnailPath(file.file_path, file.id) + if (fs.existsSync(thumbnailPath)) { + filePath = thumbnailPath + mimeType = 'image/webp' + } + } + + const fileStream = FileService.getFileStream(filePath) - // Set appropriate headers for public files - res.set({ - 'Content-Type': mimeType, - 'Content-Length': fs.statSync(filePath).size, - 'Cache-Control': 'public, max-age=31536000, immutable', // 1 year cache (content-addressable) - 'X-File-ID': file.id, - 'X-File-Hash': file.file_hash, - 'X-Public-File': 'true', - 'ETag': `"${file.file_hash}"` // Use file hash as ETag for better caching - }); - - // Force download if requested - if (download === 'true') { - res.set('Content-Disposition', `attachment; filename="${file.original_name}"`); - } else { - res.set('Content-Disposition', `inline; filename="${file.original_name}"`); - } - - // Pipe file stream to response - fileStream.pipe(res); - - fileStream.on('error', (error) => { - logger.error('Public file stream error', { - fileId: file.id, - ...routeParams, - error: error.message - }); - if (!res.headersSent) { - res.status(500).json({ - error: 'Failed to serve public file', - code: 'PUBLIC_FILE_STREAM_ERROR' - }); - } - }); - - } catch (error) { - logger.error('Failed to serve public file', { - ...routeParams, - error: error.message - }); + // Set appropriate headers for public files + res.set({ + 'Content-Type': mimeType, + 'Content-Length': fs.statSync(filePath).size, + 'Cache-Control': 'public, max-age=31536000, immutable', // 1 year cache (content-addressable) + 'X-File-ID': file.id, + 'X-File-Hash': file.file_hash, + 'X-Public-File': 'true', + 'ETag': `"${file.file_hash}"` // Use file hash as ETag for better caching + }) + + // Force download if requested + if (download === 'true') { + res.set('Content-Disposition', `attachment; filename="${file.original_name}"`) + } else { + res.set('Content-Disposition', `inline; filename="${file.original_name}"`) + } + + // Pipe file stream to response + fileStream.pipe(res) + + fileStream.on('error', (error) => { + logger.error('Public file stream error', { + fileId: file.id, + ...routeParams, + error: error.message + }) + if (!res.headersSent) { + res.status(500).json({ + error: 'Failed to serve public file', + code: 'PUBLIC_FILE_STREAM_ERROR' + }) + } + }) + + } catch (error) { + logger.error('Failed to serve public file', { + ...routeParams, + error: error.message + }) - if (error.message === 'File not found on disk') { - return res.status(404).json({ - error: 'Public file not found on disk', - code: 'PUBLIC_FILE_NOT_FOUND_ON_DISK' - }); - } - - throw error; + if (error.message === 'File not found on disk') { + return res.status(404).json({ + error: 'Public file not found on disk', + code: 'PUBLIC_FILE_NOT_FOUND_ON_DISK' + }) + } + + throw error + } } - }; - /** + /** * Serve public file by context and filename (LEGACY - backward compatibility) * Returns the most recent file with that name * No authentication required for public files */ - servePublicFileLegacy = asyncHandler(async (req, res) => { - const { context, filename } = req.params; - const { download, thumbnail } = req.query; + servePublicFileLegacy = asyncHandler(async (req, res) => { + const { context, filename } = req.params + const { download, thumbnail } = req.query - // Get public file record (latest) - const file = await FileService.getPublicFileByContextAndFilename(context, filename); + // Get public file record (latest) + const file = await FileService.getPublicFileByContextAndFilename(context, filename) - if (!file) { - return res.status(404).json({ - error: 'Public file not found', - code: 'PUBLIC_FILE_NOT_FOUND' - }); - } - - // Log legacy access for monitoring - logger.warn('Legacy public file access - consider using hash-based URLs', { - context, - filename, - fileId: file.id, - newUrl: `/public/${context}/${file.file_hash.substring(0, 12)}/${filename}` - }); - - try { - let filePath = file.file_path; - let mimeType = file.mime_type; - - // Serve thumbnail if requested - if (thumbnail === 'true') { - const thumbnailPath = FileService.getThumbnailPath(file.file_path, file.id); - if (fs.existsSync(thumbnailPath)) { - filePath = thumbnailPath; - mimeType = 'image/webp'; + if (!file) { + return res.status(404).json({ + error: 'Public file not found', + code: 'PUBLIC_FILE_NOT_FOUND' + }) } - } - const fileStream = FileService.getFileStream(filePath); + // Log legacy access for monitoring + logger.warn('Legacy public file access - consider using hash-based URLs', { + context, + filename, + fileId: file.id, + newUrl: `/public/${context}/${file.file_hash.substring(0, 12)}/${filename}` + }) + + try { + let filePath = file.file_path + let mimeType = file.mime_type + + // Serve thumbnail if requested + if (thumbnail === 'true') { + const thumbnailPath = FileService.getThumbnailPath(file.file_path, file.id) + if (fs.existsSync(thumbnailPath)) { + filePath = thumbnailPath + mimeType = 'image/webp' + } + } + + const fileStream = FileService.getFileStream(filePath) - // Set appropriate headers for public files - res.set({ - 'Content-Type': mimeType, - 'Content-Length': fs.statSync(filePath).size, - 'Cache-Control': 'public, max-age=86400', // 24 hours cache (filename-based, not immutable) - 'X-File-ID': file.id, - 'X-Legacy-Access': 'true', - 'X-Recommended-URL': `/public/${context}/${file.file_hash.substring(0, 12)}/${filename}`, - 'X-Public-File': 'true' - }); - - // Force download if requested - if (download === 'true') { - res.set('Content-Disposition', `attachment; filename="${file.original_name}"`); - } else { - res.set('Content-Disposition', `inline; filename="${file.original_name}"`); - } - - // Pipe file stream to response - fileStream.pipe(res); - - fileStream.on('error', (error) => { - logger.error('Legacy public file stream error', { - fileId: file.id, - context, - filename, - error: error.message - }); - if (!res.headersSent) { - res.status(500).json({ - error: 'Failed to serve public file', - code: 'PUBLIC_FILE_STREAM_ERROR' - }); - } - }); - - } catch (error) { - logger.error('Failed to serve legacy public file', { - context, - filename, - error: error.message - }); + // Set appropriate headers for public files + res.set({ + 'Content-Type': mimeType, + 'Content-Length': fs.statSync(filePath).size, + 'Cache-Control': 'public, max-age=86400', // 24 hours cache (filename-based, not immutable) + 'X-File-ID': file.id, + 'X-Legacy-Access': 'true', + 'X-Recommended-URL': `/public/${context}/${file.file_hash.substring(0, 12)}/${filename}`, + 'X-Public-File': 'true' + }) + + // Force download if requested + if (download === 'true') { + res.set('Content-Disposition', `attachment; filename="${file.original_name}"`) + } else { + res.set('Content-Disposition', `inline; filename="${file.original_name}"`) + } + + // Pipe file stream to response + fileStream.pipe(res) + + fileStream.on('error', (error) => { + logger.error('Legacy public file stream error', { + fileId: file.id, + context, + filename, + error: error.message + }) + if (!res.headersSent) { + res.status(500).json({ + error: 'Failed to serve public file', + code: 'PUBLIC_FILE_STREAM_ERROR' + }) + } + }) + + } catch (error) { + logger.error('Failed to serve legacy public file', { + context, + filename, + error: error.message + }) - if (error.message === 'File not found on disk') { - return res.status(404).json({ - error: 'Public file not found on disk', - code: 'PUBLIC_FILE_NOT_FOUND_ON_DISK' - }); - } - - throw error; - } - }); + if (error.message === 'File not found on disk') { + return res.status(404).json({ + error: 'Public file not found on disk', + code: 'PUBLIC_FILE_NOT_FOUND_ON_DISK' + }) + } + + throw error + } + }) - /** + /** * Serve file by context and filename (authenticated) * Falls back to ID-based lookup if context/filename not found */ - serveFileByContextAndFilename = asyncHandler(async (req, res) => { - const { context, filename } = req.params; - const { download, thumbnail } = req.query; + serveFileByContextAndFilename = asyncHandler(async (req, res) => { + const { context, filename } = req.params + const { download, thumbnail } = req.query - // First try to find by context and filename - let file = await FileService.getFileByContextAndFilename(context, filename); + // First try to find by context and filename + let file = await FileService.getFileByContextAndFilename(context, filename) - // If not found, try treating context as fileId for backward compatibility - if (!file) { - file = await FileService.getFile(context, { trackAccess: false }); + // If not found, try treating context as fileId for backward compatibility + if (!file) { + file = await FileService.getFile(context, { trackAccess: false }) - if (!file) { - return res.status(404).json({ - error: 'File not found', - code: 'FILE_NOT_FOUND' - }); - } - } + if (!file) { + return res.status(404).json({ + error: 'File not found', + code: 'FILE_NOT_FOUND' + }) + } + } - // Check access permissions - if (!FileService.validateFileAccess(file, req)) { - return res.status(403).json({ - error: 'Access denied', - code: 'ACCESS_DENIED' - }); - } + // Check access permissions + if (!FileService.validateFileAccess(file, req)) { + return res.status(403).json({ + error: 'Access denied', + code: 'ACCESS_DENIED' + }) + } - // Update access count if we found a file - if (file) { - FileService.updateAccessCount(file.id).catch(err => { - logger.warn('Failed to update access count', { - fileId: file.id, - error: err.message - }); - }); - } + // Update access count if we found a file + if (file) { + FileService.updateAccessCount(file.id).catch(err => { + logger.warn('Failed to update access count', { + fileId: file.id, + error: err.message + }) + }) + } - try { - let filePath = file.file_path; - let mimeType = file.mime_type; + try { + let filePath = file.file_path + let mimeType = file.mime_type - // Serve thumbnail if requested - if (thumbnail === 'true') { - const thumbnailPath = FileService.getThumbnailPath(file.file_path, file.id); - if (fs.existsSync(thumbnailPath)) { - filePath = thumbnailPath; - mimeType = 'image/webp'; - } - } + // Serve thumbnail if requested + if (thumbnail === 'true') { + const thumbnailPath = FileService.getThumbnailPath(file.file_path, file.id) + if (fs.existsSync(thumbnailPath)) { + filePath = thumbnailPath + mimeType = 'image/webp' + } + } - const fileStream = FileService.getFileStream(filePath); + const fileStream = FileService.getFileStream(filePath) - // Set appropriate headers - res.set({ - 'Content-Type': mimeType, - 'Content-Length': fs.statSync(filePath).size, - 'Cache-Control': file.is_public ? 'public, max-age=86400' : 'private, max-age=3600', - 'X-File-ID': file.id - }); - - // Force download if requested - if (download === 'true') { - res.set('Content-Disposition', `attachment; filename="${file.original_name}"`); - } else { - res.set('Content-Disposition', `inline; filename="${file.original_name}"`); - } - - // Pipe file stream to response - fileStream.pipe(res); - - fileStream.on('error', (error) => { - logger.error('File stream error', { - fileId: file.id, - context, - filename, - error: error.message - }); - if (!res.headersSent) { - res.status(500).json({ - error: 'Failed to serve file', - code: 'FILE_STREAM_ERROR' - }); - } - }); - - } catch (error) { - logger.error('Failed to serve file by context and filename', { - context, - filename, - error: error.message - }); + // Set appropriate headers + res.set({ + 'Content-Type': mimeType, + 'Content-Length': fs.statSync(filePath).size, + 'Cache-Control': file.is_public ? 'public, max-age=86400' : 'private, max-age=3600', + 'X-File-ID': file.id + }) + + // Force download if requested + if (download === 'true') { + res.set('Content-Disposition', `attachment; filename="${file.original_name}"`) + } else { + res.set('Content-Disposition', `inline; filename="${file.original_name}"`) + } + + // Pipe file stream to response + fileStream.pipe(res) + + fileStream.on('error', (error) => { + logger.error('File stream error', { + fileId: file.id, + context, + filename, + error: error.message + }) + if (!res.headersSent) { + res.status(500).json({ + error: 'Failed to serve file', + code: 'FILE_STREAM_ERROR' + }) + } + }) + + } catch (error) { + logger.error('Failed to serve file by context and filename', { + context, + filename, + error: error.message + }) - if (error.message === 'File not found on disk') { - return res.status(404).json({ - error: 'File not found on disk', - code: 'FILE_NOT_FOUND_ON_DISK' - }); - } - - throw error; - } - }); + if (error.message === 'File not found on disk') { + return res.status(404).json({ + error: 'File not found on disk', + code: 'FILE_NOT_FOUND_ON_DISK' + }) + } + + throw error + } + }) } -export default new FileController(); +export default new FileController() diff --git a/src/controllers/HomeController.js b/src/controllers/HomeController.js index 0c73a3e..e5cae03 100644 --- a/src/controllers/HomeController.js +++ b/src/controllers/HomeController.js @@ -1,25 +1,25 @@ // Otto File Server Homepage Controller -import { createRequire } from 'module'; -import FileModel from '../models/File.js'; +import { createRequire } from 'module' +import FileModel from '../models/File.js' -const require = createRequire(import.meta.url); -const { version } = require('../../package.json'); +const require = createRequire(import.meta.url) +const { version } = require('../../package.json') // Helper function for formatting bytes function formatBytes(bytes) { - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] } class HomeController { async home(req, res) { try { - const showStats = process.env.SHOW_STATS !== 'false'; - const stats = showStats ? await FileModel.getStats() : null; + const showStats = process.env.SHOW_STATS !== 'false' + const stats = showStats ? await FileModel.getStats() : null - const html = ` + const html = ` @@ -147,40 +147,40 @@ class HomeController { async home(req, res) { -`; +` - res.setHeader('Content-Type', 'text/html'); - res.send(html); + res.setHeader('Content-Type', 'text/html') + res.send(html) } catch (error) { - res.status(500).json({ - error: 'Failed to load homepage', - message: error.message - }); + res.status(500).json({ + error: 'Failed to load homepage', + message: error.message + }) } } - formatBytes(bytes) { - return formatBytes(bytes); - } +formatBytes(bytes) { + return formatBytes(bytes) +} - async stats(req, res) { +async stats(req, res) { try { - const stats = await FileModel.getStats(); - res.json({ - success: true, - data: { - ...stats, - total_size_formatted: formatBytes(stats.total_size || 0), - version: version || '1.0.0' - } - }); + const stats = await FileModel.getStats() + res.json({ + success: true, + data: { + ...stats, + total_size_formatted: formatBytes(stats.total_size || 0), + version: version || '1.0.0' + } + }) } catch (error) { - res.status(500).json({ - success: false, - error: 'Failed to get stats', - message: error.message - }); + res.status(500).json({ + success: false, + error: 'Failed to get stats', + message: error.message + }) } - } +} } -export default new HomeController(); +export default new HomeController() diff --git a/src/controllers/UploadController.js b/src/controllers/UploadController.js index 12cbb57..fdf1d22 100644 --- a/src/controllers/UploadController.js +++ b/src/controllers/UploadController.js @@ -1,255 +1,256 @@ -import FileService from '../services/FileService.js'; -import TokenService from '../services/TokenService.js'; -import ChunkedUploadService from '../services/ChunkedUploadService.js'; -import { asyncHandler } from '../middleware/errorHandler.js'; -import logger from '../config/logger.js'; +import FileService from '../services/FileService.js' +import TokenService from '../services/TokenService.js' +import ChunkedUploadService from '../services/ChunkedUploadService.js' +import { asyncHandler } from '../middleware/errorHandler.js' +import logger from '../config/logger.js' class UploadController { generateUploadToken = asyncHandler(async (req, res) => { const { - context = 'general', - uploadedBy, - maxFiles = 5, - maxSize, - allowedTypes, - expiresIn - } = req.body; + context = 'general', + uploadedBy, + maxFiles = 5, + maxSize, + allowedTypes, + expiresIn + } = req.body // Validate required fields if (!uploadedBy) { - return res.status(400).json({ - error: 'uploadedBy is required', - code: 'MISSING_UPLOADED_BY' - }); + return res.status(400).json({ + error: 'uploadedBy is required', + code: 'MISSING_UPLOADED_BY' + }) } const tokenData = TokenService.generateUploadToken({ - context, - uploadedBy, - maxFiles: parseInt(maxFiles) || 5, - maxSize: parseInt(maxSize), - allowedTypes: allowedTypes ? allowedTypes.split(',').map(t => t.trim()) : null, - expiresIn - }); + context, + uploadedBy, + maxFiles: parseInt(maxFiles) || 5, + maxSize: parseInt(maxSize), + allowedTypes: allowedTypes ? allowedTypes.split(',').map(t => t.trim()) : null, + expiresIn + }) logger.info('Upload token generated for user', { - uploadedBy, - context, - tokenId: tokenData.tokenId - }); + uploadedBy, + context, + tokenId: tokenData.tokenId + }) res.json({ - success: true, - data: tokenData - }); - }); - uploadFiles = asyncHandler(async (req, res) => { + success: true, + data: tokenData + }) +}) +uploadFiles = asyncHandler(async (req, res) => { if (!req.files || req.files.length === 0) { - return res.status(400).json({ - error: 'No files provided', - code: 'NO_FILES' - }); + return res.status(400).json({ + error: 'No files provided', + code: 'NO_FILES' + }) } // Check if any files are too large for regular upload and suggest chunked upload - const chunkSize = ChunkedUploadService.chunkSize; - const largeFiles = req.files.filter(file => file.size > chunkSize); + const chunkSize = ChunkedUploadService.chunkSize + const largeFiles = req.files.filter(file => file.size > chunkSize) if (largeFiles.length > 0) { - logger.info('Large files detected, suggesting chunked upload', { - fileCount: largeFiles.length, - fileSizes: largeFiles.map(f => f.size), - chunkSize - }); + logger.info('Large files detected, suggesting chunked upload', { + fileCount: largeFiles.length, + fileSizes: largeFiles.map(f => f.size), + chunkSize + }) - return res.status(413).json({ - error: 'Files too large for regular upload', - code: 'FILES_TOO_LARGE_FOR_REGULAR_UPLOAD', - suggestion: 'Use chunked upload API for files larger than ' + Math.round(chunkSize / 1024 / 1024) + 'MB', - chunkUploadEndpoint: '/upload/chunk', - chunkSize, - largeFiles: largeFiles.map(f => ({ - name: f.originalname, - size: f.size - })) - }); + return res.status(413).json({ + error: 'Files too large for regular upload', + code: 'FILES_TOO_LARGE_FOR_REGULAR_UPLOAD', + suggestion: 'Use chunked upload API for files larger than ' + Math.round(chunkSize / 1024 / 1024) + 'MB', + chunkUploadEndpoint: '/upload/chunk', + chunkSize, + largeFiles: largeFiles.map(f => ({ + name: f.originalname, + size: f.size + })) + }) } // Determine upload context and user - let context = req.body.context || 'general'; - let uploadedBy = 'system'; - let uploadSource = 'api'; + let context = req.body.context || 'general' + let uploadedBy = 'system' + let uploadSource = 'api' if (req.authenticationType === 'service') { - uploadedBy = req.body.uploadedBy || 'service'; - uploadSource = 'service'; + uploadedBy = req.body.uploadedBy || 'service' + uploadSource = 'service' } else if (req.authenticationType === 'upload_token') { - context = req.uploadToken.context; - uploadedBy = req.uploadToken.uploadedBy; - uploadSource = 'frontend'; + context = req.uploadToken.context + uploadedBy = req.uploadToken.uploadedBy + uploadSource = 'frontend' - // Validate token constraints - if (req.files.length > req.uploadToken.maxFiles) { - return res.status(400).json({ - error: `Too many files. Token allows maximum ${req.uploadToken.maxFiles} files`, - code: 'TOKEN_FILE_LIMIT_EXCEEDED' - }); - } - - // Check file sizes against token limit - const totalSize = req.files.reduce((sum, file) => sum + file.size, 0); - if (totalSize > req.uploadToken.maxSize) { - return res.status(400).json({ - error: `Total file size exceeds token limit of ${req.uploadToken.maxSize} bytes`, - code: 'TOKEN_SIZE_LIMIT_EXCEEDED' - }); - } - - // Check allowed types if specified in token - if (req.uploadToken.allowedTypes) { - const invalidFiles = req.files.filter(file => - !req.uploadToken.allowedTypes.includes(file.mimetype) - ); + // Validate token constraints + if (req.files.length > req.uploadToken.maxFiles) { + return res.status(400).json({ + error: `Too many files. Token allows maximum ${req.uploadToken.maxFiles} files`, + code: 'TOKEN_FILE_LIMIT_EXCEEDED' + }) + } + + // Check file sizes against token limit + const totalSize = req.files.reduce((sum, file) => sum + file.size, 0) + if (totalSize > req.uploadToken.maxSize) { + return res.status(400).json({ + error: `Total file size exceeds token limit of ${req.uploadToken.maxSize} bytes`, + code: 'TOKEN_SIZE_LIMIT_EXCEEDED' + }) + } + + // Check allowed types if specified in token + if (req.uploadToken.allowedTypes) { + const invalidFiles = req.files.filter(file => + !req.uploadToken.allowedTypes.includes(file.mimetype) + ) - if (invalidFiles.length > 0) { - return res.status(400).json({ - error: 'Some files have types not allowed by this token', - code: 'TOKEN_TYPE_NOT_ALLOWED', - invalidFiles: invalidFiles.map(f => f.originalname) - }); + if (invalidFiles.length > 0) { + return res.status(400).json({ + error: 'Some files have types not allowed by this token', + code: 'TOKEN_TYPE_NOT_ALLOWED', + invalidFiles: invalidFiles.map(f => f.originalname) + }) + } } - } } else if (req.authenticationType === 'jwt') { - uploadedBy = req.user.sub || req.user.id; - uploadSource = 'user'; + uploadedBy = req.user.sub || req.user.id + uploadSource = 'user' } // Parse additional metadata - let metadata = {}; + let metadata = {} if (req.body.metadata) { - try { - metadata = JSON.parse(req.body.metadata); - } catch (error) { - logger.warn('Invalid metadata JSON', { metadata: req.body.metadata }); - } + try { + metadata = JSON.parse(req.body.metadata) + } catch (error) { + logger.warn('Invalid metadata JSON', { metadata: req.body.metadata }) + logger.warn(`Metadata JSON parse error: ${error.message}`, 'debug') + } } // Process uploaded files const processedFiles = await FileService.processUploadedFiles(req.files, { - context, - uploadedBy, - uploadSource, - generateThumbnails: req.body.generateThumbnails === 'true', - metadata - }); + context, + uploadedBy, + uploadSource, + generateThumbnails: req.body.generateThumbnails === 'true', + metadata + }) logger.info('Files uploaded successfully', { - count: processedFiles.length, - uploadedBy, - context, - fileIds: processedFiles.map(f => f.id) + count: processedFiles.length, + uploadedBy, + context, + fileIds: processedFiles.map(f => f.id) }); res.status(201).json({ - success: true, - data: { files: processedFiles.map(file => { - const hashPrefix = file.file_hash.substring(0, 12); - const fileExt = file.original_name.split('.').pop().toLowerCase(); + success: true, + data: { files: processedFiles.map(file => { + const hashPrefix = file.file_hash.substring(0, 12) + const fileExt = file.original_name.split('.').pop().toLowerCase() - return { - id: file.id, - filename: file.filename, - originalName: file.original_name, - mimeType: file.mime_type, - fileSize: file.file_size, - uploadContext: file.upload_context, - uploadedAt: file.created_at, - isPublic: file.is_public, - url: `/files/${file.id}`, - publicUrl: file.is_public ? `/public/${file.upload_context}/${hashPrefix}` : null, - publicUrlWithExt: file.is_public ? `/public/${file.upload_context}/${hashPrefix}.${fileExt}` : null, - shortPublicUrl: file.is_public ? `/p/${file.upload_context}/${hashPrefix}` : null, - shortPublicUrlWithExt: file.is_public ? `/p/${file.upload_context}/${hashPrefix}.${fileExt}` : null, - legacyPublicUrl: file.is_public ? `/public/${file.upload_context}/${file.original_name}` : null - }; + return { + id: file.id, + filename: file.filename, + originalName: file.original_name, + mimeType: file.mime_type, + fileSize: file.file_size, + uploadContext: file.upload_context, + uploadedAt: file.created_at, + isPublic: file.is_public, + url: `/files/${file.id}`, + publicUrl: file.is_public ? `/public/${file.upload_context}/${hashPrefix}` : null, + publicUrlWithExt: file.is_public ? `/public/${file.upload_context}/${hashPrefix}.${fileExt}` : null, + shortPublicUrl: file.is_public ? `/p/${file.upload_context}/${hashPrefix}` : null, + shortPublicUrlWithExt: file.is_public ? `/p/${file.upload_context}/${hashPrefix}.${fileExt}` : null, + legacyPublicUrl: file.is_public ? `/public/${file.upload_context}/${file.original_name}` : null + } }), count: processedFiles.length, totalSize: processedFiles.reduce((sum, file) => sum + file.file_size, 0) - } - }); - }); + } + }) +}) - /** +/** * Get upload statistics */ - getUploadStats = asyncHandler(async (req, res) => { - const stats = await FileService.getStats(); +getUploadStats = asyncHandler(async (req, res) => { + const stats = await FileService.getStats() res.json({ - success: true, - data: { - totalFiles: parseInt(stats.total_files) || 0, - totalSize: parseInt(stats.total_size) || 0, - averageSize: parseFloat(stats.avg_size) || 0, - uniqueContexts: parseInt(stats.unique_contexts) || 0, - uniqueUploaders: parseInt(stats.unique_uploaders) || 0, - formattedTotalSize: this.formatBytes(parseInt(stats.total_size) || 0) - } - }); - }); - - /** + success: true, + data: { + totalFiles: parseInt(stats.total_files) || 0, + totalSize: parseInt(stats.total_size) || 0, + averageSize: parseFloat(stats.avg_size) || 0, + uniqueContexts: parseInt(stats.unique_contexts) || 0, + uniqueUploaders: parseInt(stats.unique_uploaders) || 0, + formattedTotalSize: this.formatBytes(parseInt(stats.total_size) || 0) + } + }) +}) + +/** * Get files by context */ - getFilesByContext = asyncHandler(async (req, res) => { - const { context } = req.params; - const { limit = 50, offset = 0 } = req.query; +getFilesByContext = asyncHandler(async (req, res) => { + const { context } = req.params + const { limit = 50, offset = 0 } = req.query const files = await FileService.getFilesByContext(context, { - limit: parseInt(limit), - offset: parseInt(offset) + limit: parseInt(limit), + offset: parseInt(offset) }); res.json({ - success: true, - data: { files: files.map(file => { - const hashPrefix = file.file_hash.substring(0, 12); - const fileExt = file.original_name.split('.').pop().toLowerCase(); + success: true, + data: { files: files.map(file => { + const hashPrefix = file.file_hash.substring(0, 12) + const fileExt = file.original_name.split('.').pop().toLowerCase() - return { - id: file.id, - filename: file.filename, - originalName: file.original_name, - mimeType: file.mime_type, - fileSize: file.file_size, - uploadContext: file.upload_context, - uploadedBy: file.uploaded_by, - uploadedAt: file.created_at, - accessCount: file.access_count, - url: `/files/${file.id}`, - publicUrl: file.is_public ? `/public/${file.upload_context}/${hashPrefix}` : null, - publicUrlWithExt: file.is_public ? `/public/${file.upload_context}/${hashPrefix}.${fileExt}` : null, - shortPublicUrl: file.is_public ? `/p/${file.upload_context}/${hashPrefix}` : null, - shortPublicUrlWithExt: file.is_public ? `/p/${file.upload_context}/${hashPrefix}.${fileExt}` : null, - legacyPublicUrl: file.is_public ? `/public/${file.upload_context}/${file.original_name}` : null - }; + return { + id: file.id, + filename: file.filename, + originalName: file.original_name, + mimeType: file.mime_type, + fileSize: file.file_size, + uploadContext: file.upload_context, + uploadedBy: file.uploaded_by, + uploadedAt: file.created_at, + accessCount: file.access_count, + url: `/files/${file.id}`, + publicUrl: file.is_public ? `/public/${file.upload_context}/${hashPrefix}` : null, + publicUrlWithExt: file.is_public ? `/public/${file.upload_context}/${hashPrefix}.${fileExt}` : null, + shortPublicUrl: file.is_public ? `/p/${file.upload_context}/${hashPrefix}` : null, + shortPublicUrlWithExt: file.is_public ? `/p/${file.upload_context}/${hashPrefix}.${fileExt}` : null, + legacyPublicUrl: file.is_public ? `/public/${file.upload_context}/${file.original_name}` : null + } }), context, count: files.length, pagination: { - limit: parseInt(limit), - offset: parseInt(offset), - hasMore: files.length === parseInt(limit) + limit: parseInt(limit), + offset: parseInt(offset), + hasMore: files.length === parseInt(limit) } - } - }); - }); + } + }) +}) - /** +/** * Format bytes to human readable format */ - formatBytes(bytes) { - if (bytes === 0) return '0 Bytes'; - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; - } +formatBytes(bytes) { + if (bytes === 0) return '0 Bytes' + const k = 1024 + const sizes = ['Bytes', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] +} } -export default new UploadController(); +export default new UploadController() diff --git a/src/middleware/auth.js b/src/middleware/auth.js index 097409d..d91043b 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -1,191 +1,191 @@ -import jwt from 'jsonwebtoken'; -import logger from '../config/logger.js'; +import jwt from 'jsonwebtoken' +import logger from '../config/logger.js' // Service token authentication for backend-to-backend communication export const authenticateService = (req, res, next) => { - const authHeader = req.headers.authorization; - const serviceToken = process.env.SERVICE_TOKEN; - - if (!authHeader || !authHeader.startsWith('Bearer ')) { - return res.status(401).json({ - error: 'Missing or invalid authorization header', - code: 'MISSING_AUTH_HEADER' - }); - } - - const token = authHeader.substring(7); - - if (!serviceToken || token !== serviceToken) { - logger.warn('Invalid service token attempt', { - ip: req.ip, - userAgent: req.get('User-Agent') - }); - return res.status(401).json({ - error: 'Invalid service token', - code: 'INVALID_SERVICE_TOKEN' - }); - } - - req.authenticationType = 'service'; - req.serviceAuthenticated = true; - logger.debug('Service authentication successful', { ip: req.ip }); - next(); -}; + const authHeader = req.headers.authorization + const serviceToken = process.env.SERVICE_TOKEN + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ + error: 'Missing or invalid authorization header', + code: 'MISSING_AUTH_HEADER' + }) + } + + const token = authHeader.substring(7) + + if (!serviceToken || token !== serviceToken) { + logger.warn('Invalid service token attempt', { + ip: req.ip, + userAgent: req.get('User-Agent') + }) + return res.status(401).json({ + error: 'Invalid service token', + code: 'INVALID_SERVICE_TOKEN' + }) + } + + req.authenticationType = 'service' + req.serviceAuthenticated = true + logger.debug('Service authentication successful', { ip: req.ip }) + next() +} // JWT token authentication for frontend uploads export const authenticateJWT = (req, res, next) => { - const authHeader = req.headers.authorization; - - if (!authHeader || !authHeader.startsWith('Bearer ')) { - return res.status(401).json({ - error: 'Missing or invalid authorization header', - code: 'MISSING_AUTH_HEADER' - }); - } - - const token = authHeader.substring(7); - - try { - const decoded = jwt.verify(token, process.env.JWT_SECRET); - req.user = decoded; - req.authenticationType = 'jwt'; - logger.debug('JWT authentication successful', { - userId: decoded.sub || decoded.id, - ip: req.ip - }); - next(); - } catch (error) { - logger.warn('JWT authentication failed', { - error: error.message, - ip: req.ip, - userAgent: req.get('User-Agent') - }); - - if (error.name === 'TokenExpiredError') { - return res.status(401).json({ - error: 'Token expired', - code: 'TOKEN_EXPIRED' - }); + const authHeader = req.headers.authorization + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ + error: 'Missing or invalid authorization header', + code: 'MISSING_AUTH_HEADER' + }) } - return res.status(401).json({ - error: 'Invalid token', - code: 'INVALID_TOKEN' - }); - } -}; + const token = authHeader.substring(7) + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET) + req.user = decoded + req.authenticationType = 'jwt' + logger.debug('JWT authentication successful', { + userId: decoded.sub || decoded.id, + ip: req.ip + }) + next() + } catch (error) { + logger.warn('JWT authentication failed', { + error: error.message, + ip: req.ip, + userAgent: req.get('User-Agent') + }) + + if (error.name === 'TokenExpiredError') { + return res.status(401).json({ + error: 'Token expired', + code: 'TOKEN_EXPIRED' + }) + } + + return res.status(401).json({ + error: 'Invalid token', + code: 'INVALID_TOKEN' + }) + } +} // Upload token authentication for temporary upload permissions export const authenticateUploadToken = (req, res, next) => { - const authHeader = req.headers.authorization; + const authHeader = req.headers.authorization - if (!authHeader || !authHeader.startsWith('Bearer ')) { - return res.status(401).json({ - error: 'Missing or invalid authorization header', - code: 'MISSING_AUTH_HEADER' - }); - } + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ + error: 'Missing or invalid authorization header', + code: 'MISSING_AUTH_HEADER' + }) + } - const token = authHeader.substring(7); + const token = authHeader.substring(7) - try { - const decoded = jwt.verify(token, process.env.JWT_SECRET); + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET) - // Check if this is specifically an upload token - if (decoded.type !== 'upload') { - return res.status(401).json({ - error: 'Invalid token type', - code: 'INVALID_TOKEN_TYPE' - }); + // Check if this is specifically an upload token + if (decoded.type !== 'upload') { + return res.status(401).json({ + error: 'Invalid token type', + code: 'INVALID_TOKEN_TYPE' + }) + } + + req.uploadToken = decoded + req.authenticationType = 'upload_token' + logger.debug('Upload token authentication successful', { + tokenId: decoded.jti, + context: decoded.context, + ip: req.ip + }) + next() + } catch (error) { + logger.warn('Upload token authentication failed', { + error: error.message, + ip: req.ip + }) + + if (error.name === 'TokenExpiredError') { + return res.status(401).json({ + error: 'Upload token expired', + code: 'UPLOAD_TOKEN_EXPIRED' + }) + } + + return res.status(401).json({ + error: 'Invalid upload token', + code: 'INVALID_UPLOAD_TOKEN' + }) } +} + +// Flexible authentication - accepts service token, JWT, or upload token +export const authenticate = (req, res, next) => { + const authHeader = req.headers.authorization - req.uploadToken = decoded; - req.authenticationType = 'upload_token'; - logger.debug('Upload token authentication successful', { - tokenId: decoded.jti, - context: decoded.context, - ip: req.ip - }); - next(); - } catch (error) { - logger.warn('Upload token authentication failed', { - error: error.message, - ip: req.ip - }); - - if (error.name === 'TokenExpiredError') { - return res.status(401).json({ - error: 'Upload token expired', - code: 'UPLOAD_TOKEN_EXPIRED' - }); + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ + error: 'Missing or invalid authorization header', + code: 'MISSING_AUTH_HEADER' + }) } - return res.status(401).json({ - error: 'Invalid upload token', - code: 'INVALID_UPLOAD_TOKEN' - }); - } -}; + const token = authHeader.substring(7) + const serviceToken = process.env.SERVICE_TOKEN -// Flexible authentication - accepts service token, JWT, or upload token -export const authenticate = (req, res, next) => { - const authHeader = req.headers.authorization; - - if (!authHeader || !authHeader.startsWith('Bearer ')) { - return res.status(401).json({ - error: 'Missing or invalid authorization header', - code: 'MISSING_AUTH_HEADER' - }); - } - - const token = authHeader.substring(7); - const serviceToken = process.env.SERVICE_TOKEN; - - // Try service token first - if (serviceToken && token === serviceToken) { - req.authenticationType = 'service'; - req.serviceAuthenticated = true; - logger.debug('Service authentication successful', { ip: req.ip }); - return next(); - } - - // Try JWT tokens (regular or upload) - try { - const decoded = jwt.verify(token, process.env.JWT_SECRET); - - if (decoded.type === 'upload') { - req.uploadToken = decoded; - req.authenticationType = 'upload_token'; - logger.debug('Upload token authentication successful', { - tokenId: decoded.jti, - ip: req.ip - }); - } else { - req.user = decoded; - req.authenticationType = 'jwt'; - logger.debug('JWT authentication successful', { - userId: decoded.sub || decoded.id, - ip: req.ip - }); + // Try service token first + if (serviceToken && token === serviceToken) { + req.authenticationType = 'service' + req.serviceAuthenticated = true + logger.debug('Service authentication successful', { ip: req.ip }) + return next() } + + // Try JWT tokens (regular or upload) + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET) - next(); - } catch (error) { - logger.warn('Authentication failed', { - error: error.message, - ip: req.ip - }); - - if (error.name === 'TokenExpiredError') { - return res.status(401).json({ - error: 'Token expired', - code: 'TOKEN_EXPIRED' - }); + if (decoded.type === 'upload') { + req.uploadToken = decoded + req.authenticationType = 'upload_token' + logger.debug('Upload token authentication successful', { + tokenId: decoded.jti, + ip: req.ip + }) + } else { + req.user = decoded + req.authenticationType = 'jwt' + logger.debug('JWT authentication successful', { + userId: decoded.sub || decoded.id, + ip: req.ip + }) + } + + next() + } catch (error) { + logger.warn('Authentication failed', { + error: error.message, + ip: req.ip + }) + + if (error.name === 'TokenExpiredError') { + return res.status(401).json({ + error: 'Token expired', + code: 'TOKEN_EXPIRED' + }) + } + + return res.status(401).json({ + error: 'Invalid authentication credentials', + code: 'INVALID_CREDENTIALS' + }) } - - return res.status(401).json({ - error: 'Invalid authentication credentials', - code: 'INVALID_CREDENTIALS' - }); - } -}; +} diff --git a/src/middleware/chunkUpload.js b/src/middleware/chunkUpload.js index 23bd049..c79475e 100644 --- a/src/middleware/chunkUpload.js +++ b/src/middleware/chunkUpload.js @@ -1,196 +1,196 @@ -import multer from 'multer'; -import path from 'path'; -import fs from 'fs'; -import { fileURLToPath } from 'url'; -import crypto from 'crypto'; -import logger from '../config/logger.js'; +import multer from 'multer' +import path from 'path' +import fs from 'fs' +import { fileURLToPath } from 'url' +import crypto from 'crypto' +import logger from '../config/logger.js' -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) // Ensure chunk temp directory exists -const chunkTempDir = process.env.CHUNK_TEMP_DIR || path.join(__dirname, '../../temp-chunks'); +const chunkTempDir = process.env.CHUNK_TEMP_DIR || path.join(__dirname, '../../temp-chunks') if (!fs.existsSync(chunkTempDir)) { - fs.mkdirSync(chunkTempDir, { recursive: true }); + fs.mkdirSync(chunkTempDir, { recursive: true }) } // Chunk size limit (should be larger than expected chunk size to allow for overhead) -const maxChunkSize = parseInt(process.env.MAX_CHUNK_SIZE) || 30 * 1024 * 1024; // 30MB to allow 25MB chunks +const maxChunkSize = parseInt(process.env.MAX_CHUNK_SIZE) || 30 * 1024 * 1024 // 30MB to allow 25MB chunks // Create storage configuration for chunks const chunkStorage = multer.diskStorage({ - destination: (req, file, cb) => { + destination: (req, file, cb) => { // Use general temp directory for chunks - they'll be organized by session - cb(null, chunkTempDir); - }, - filename: (req, file, cb) => { + cb(null, chunkTempDir) + }, + filename: (req, file, cb) => { // Generate temporary filename for chunk - const tempId = crypto.randomUUID(); - const extension = path.extname(file.originalname).toLowerCase() || '.chunk'; - const filename = `temp-${tempId}${extension}`; - cb(null, filename); - } -}); + const tempId = crypto.randomUUID() + const extension = path.extname(file.originalname).toLowerCase() || '.chunk' + const filename = `temp-${tempId}${extension}` + cb(null, filename) + } +}) // File filter for chunks - more permissive than regular uploads const chunkFileFilter = async (req, file, cb) => { - try { + try { // For chunks, we're less strict about MIME types since it's partial data // The final assembled file will be validated properly - // Just ensure it's not a dangerous executable - const extension = path.extname(file.originalname).toLowerCase(); - const dangerousExtensions = [ - '.exe', '.bat', '.cmd', '.com', '.pif', '.scr', '.vbs', '.js', '.jar', - '.sh', '.ps1', '.php', '.asp', '.aspx', '.jsp', '.py', '.rb', '.pl' - ]; + // Just ensure it's not a dangerous executable + const extension = path.extname(file.originalname).toLowerCase() + const dangerousExtensions = [ + '.exe', '.bat', '.cmd', '.com', '.pif', '.scr', '.vbs', '.js', '.jar', + '.sh', '.ps1', '.php', '.asp', '.aspx', '.jsp', '.py', '.rb', '.pl' + ] - if (dangerousExtensions.includes(extension)) { - logger.warn('Blocked dangerous file extension in chunk upload', { - filename: file.originalname, - extension, - ip: req.ip - }); - return cb(new Error(`File type ${extension} is not allowed for security reasons`)); + if (dangerousExtensions.includes(extension)) { + logger.warn('Blocked dangerous file extension in chunk upload', { + filename: file.originalname, + extension, + ip: req.ip + }) + return cb(new Error(`File type ${extension} is not allowed for security reasons`)) + } + + cb(null, true) + } catch (error) { + logger.error('Chunk file filter error', { error: error.message }) + cb(error) } - - cb(null, true); - } catch (error) { - logger.error('Chunk file filter error', { error: error.message }); - cb(error); - } -}; +} // Create multer upload middleware for chunks const chunkUpload = multer({ - storage: chunkStorage, - fileFilter: chunkFileFilter, - limits: { - fileSize: maxChunkSize, - files: 1, // Only one chunk per request - fields: 5, - fieldNameSize: 100, - fieldSize: 1024 // 1KB for form fields - } -}); + storage: chunkStorage, + fileFilter: chunkFileFilter, + limits: { + fileSize: maxChunkSize, + files: 1, // Only one chunk per request + fields: 5, + fieldNameSize: 100, + fieldSize: 1024 // 1KB for form fields + } +}) // Middleware to handle chunk upload errors export const handleChunkUploadError = (err, req, res, next) => { - if (err instanceof multer.MulterError) { - logger.warn('Multer chunk upload error', { - error: err.message, - code: err.code, - ip: req.ip - }); - - switch (err.code) { - case 'LIMIT_FILE_SIZE': - return res.status(400).json({ - error: `Chunk too large. Maximum chunk size is ${Math.round(maxChunkSize / 1024 / 1024)}MB`, - code: 'CHUNK_TOO_LARGE' - }); - case 'LIMIT_FILE_COUNT': - return res.status(400).json({ - error: 'Only one chunk per request allowed', - code: 'TOO_MANY_CHUNKS' - }); - case 'LIMIT_UNEXPECTED_FILE': - return res.status(400).json({ - error: 'Unexpected file field', - code: 'UNEXPECTED_CHUNK_FIELD' - }); - default: - return res.status(400).json({ - error: err.message, - code: 'CHUNK_UPLOAD_ERROR' - }); + if (err instanceof multer.MulterError) { + logger.warn('Multer chunk upload error', { + error: err.message, + code: err.code, + ip: req.ip + }) + + switch (err.code) { + case 'LIMIT_FILE_SIZE': + return res.status(400).json({ + error: `Chunk too large. Maximum chunk size is ${Math.round(maxChunkSize / 1024 / 1024)}MB`, + code: 'CHUNK_TOO_LARGE' + }) + case 'LIMIT_FILE_COUNT': + return res.status(400).json({ + error: 'Only one chunk per request allowed', + code: 'TOO_MANY_CHUNKS' + }) + case 'LIMIT_UNEXPECTED_FILE': + return res.status(400).json({ + error: 'Unexpected file field', + code: 'UNEXPECTED_CHUNK_FIELD' + }) + default: + return res.status(400).json({ + error: err.message, + code: 'CHUNK_UPLOAD_ERROR' + }) + } } - } - if (err.message.includes('not allowed')) { - return res.status(400).json({ - error: err.message, - code: 'CHUNK_FILE_TYPE_NOT_ALLOWED' - }); - } + if (err.message.includes('not allowed')) { + return res.status(400).json({ + error: err.message, + code: 'CHUNK_FILE_TYPE_NOT_ALLOWED' + }) + } - next(err); -}; + next(err) +} // Middleware to validate chunk parameters export const validateChunkParams = (req, res, next) => { - const { sessionId, chunkIndex } = req.params; + const { sessionId, chunkIndex } = req.params - if (!sessionId) { - return res.status(400).json({ - error: 'Session ID is required', - code: 'MISSING_SESSION_ID' - }); - } - - const chunkIndexNum = parseInt(chunkIndex); - if (isNaN(chunkIndexNum) || chunkIndexNum < 0) { - return res.status(400).json({ - error: 'Valid chunk index is required', - code: 'INVALID_CHUNK_INDEX' - }); - } - - // Add parsed values to request - req.sessionId = sessionId; - req.chunkIndex = chunkIndexNum; + if (!sessionId) { + return res.status(400).json({ + error: 'Session ID is required', + code: 'MISSING_SESSION_ID' + }) + } + + const chunkIndexNum = parseInt(chunkIndex) + if (isNaN(chunkIndexNum) || chunkIndexNum < 0) { + return res.status(400).json({ + error: 'Valid chunk index is required', + code: 'INVALID_CHUNK_INDEX' + }) + } + + // Add parsed values to request + req.sessionId = sessionId + req.chunkIndex = chunkIndexNum - next(); -}; + next() +} // Middleware to validate session initialization data export const validateSessionInit = (req, res, next) => { - const { originalFilename, totalSize, mimeType } = req.body; + const { originalFilename, totalSize, mimeType } = req.body - if (!originalFilename || typeof originalFilename !== 'string') { - return res.status(400).json({ - error: 'originalFilename is required and must be a string', - code: 'INVALID_FILENAME' - }); - } - - const totalSizeNum = parseInt(totalSize); - if (!totalSize || isNaN(totalSizeNum) || totalSizeNum <= 0) { - return res.status(400).json({ - error: 'totalSize is required and must be a positive number', - code: 'INVALID_TOTAL_SIZE' - }); - } - - if (!mimeType || typeof mimeType !== 'string') { - return res.status(400).json({ - error: 'mimeType is required and must be a string', - code: 'INVALID_MIME_TYPE' - }); - } - - // Validate filename extension - const extension = path.extname(originalFilename).toLowerCase(); - const dangerousExtensions = [ - '.exe', '.bat', '.cmd', '.com', '.pif', '.scr', '.vbs', '.js', '.jar', - '.sh', '.ps1', '.php', '.asp', '.aspx', '.jsp', '.py', '.rb', '.pl' - ]; + if (!originalFilename || typeof originalFilename !== 'string') { + return res.status(400).json({ + error: 'originalFilename is required and must be a string', + code: 'INVALID_FILENAME' + }) + } + + const totalSizeNum = parseInt(totalSize) + if (!totalSize || isNaN(totalSizeNum) || totalSizeNum <= 0) { + return res.status(400).json({ + error: 'totalSize is required and must be a positive number', + code: 'INVALID_TOTAL_SIZE' + }) + } + + if (!mimeType || typeof mimeType !== 'string') { + return res.status(400).json({ + error: 'mimeType is required and must be a string', + code: 'INVALID_MIME_TYPE' + }) + } + + // Validate filename extension + const extension = path.extname(originalFilename).toLowerCase() + const dangerousExtensions = [ + '.exe', '.bat', '.cmd', '.com', '.pif', '.scr', '.vbs', '.js', '.jar', + '.sh', '.ps1', '.php', '.asp', '.aspx', '.jsp', '.py', '.rb', '.pl' + ] - if (dangerousExtensions.includes(extension)) { - return res.status(400).json({ - error: `File type ${extension} is not allowed for security reasons`, - code: 'DANGEROUS_FILE_TYPE' - }); - } - - // Add parsed values to request - req.totalSize = totalSizeNum; + if (dangerousExtensions.includes(extension)) { + return res.status(400).json({ + error: `File type ${extension} is not allowed for security reasons`, + code: 'DANGEROUS_FILE_TYPE' + }) + } + + // Add parsed values to request + req.totalSize = totalSizeNum - next(); -}; + next() +} // Export configured upload middleware -export const uploadSingleChunk = chunkUpload.single('chunk'); +export const uploadSingleChunk = chunkUpload.single('chunk') // Export configuration -export { maxChunkSize, chunkTempDir }; +export { maxChunkSize, chunkTempDir } diff --git a/src/middleware/errorHandler.js b/src/middleware/errorHandler.js index 6600f12..b730060 100644 --- a/src/middleware/errorHandler.js +++ b/src/middleware/errorHandler.js @@ -1,86 +1,86 @@ -import logger from '../config/logger.js'; +import logger from '../config/logger.js' export const errorHandler = (err, req, res, next) => { - logger.error('Unhandled error', { - error: err.message, - stack: err.stack, - url: req.url, - method: req.method, - ip: req.ip, - userAgent: req.get('User-Agent') - }); + logger.error('Unhandled error', { + error: err.message, + stack: err.stack, + url: req.url, + method: req.method, + ip: req.ip, + userAgent: req.get('User-Agent') + }) - // Don't leak error details in production - const isDevelopment = process.env.NODE_ENV === 'development'; + // Don't leak error details in production + const isDevelopment = process.env.NODE_ENV === 'development' - if (err.name === 'ValidationError') { - return res.status(400).json({ - error: 'Validation failed', - code: 'VALIDATION_ERROR', - details: isDevelopment ? err.details : undefined - }); - } + if (err.name === 'ValidationError') { + return res.status(400).json({ + error: 'Validation failed', + code: 'VALIDATION_ERROR', + details: isDevelopment ? err.details : undefined + }) + } - if (err.name === 'UnauthorizedError') { - return res.status(401).json({ - error: 'Unauthorized', - code: 'UNAUTHORIZED' - }); - } + if (err.name === 'UnauthorizedError') { + return res.status(401).json({ + error: 'Unauthorized', + code: 'UNAUTHORIZED' + }) + } - if (err.name === 'ForbiddenError') { - return res.status(403).json({ - error: 'Forbidden', - code: 'FORBIDDEN' - }); - } + if (err.name === 'ForbiddenError') { + return res.status(403).json({ + error: 'Forbidden', + code: 'FORBIDDEN' + }) + } - if (err.name === 'NotFoundError') { - return res.status(404).json({ - error: 'Resource not found', - code: 'NOT_FOUND' - }); - } + if (err.name === 'NotFoundError') { + return res.status(404).json({ + error: 'Resource not found', + code: 'NOT_FOUND' + }) + } - // Database errors - if (err.code === '23505') { // PostgreSQL unique constraint violation - return res.status(409).json({ - error: 'Resource already exists', - code: 'DUPLICATE_RESOURCE' - }); - } + // Database errors + if (err.code === '23505') { // PostgreSQL unique constraint violation + return res.status(409).json({ + error: 'Resource already exists', + code: 'DUPLICATE_RESOURCE' + }) + } - if (err.code === '23503') { // PostgreSQL foreign key violation - return res.status(400).json({ - error: 'Invalid reference', - code: 'INVALID_REFERENCE' - }); - } + if (err.code === '23503') { // PostgreSQL foreign key violation + return res.status(400).json({ + error: 'Invalid reference', + code: 'INVALID_REFERENCE' + }) + } - // Default error response - res.status(500).json({ - error: isDevelopment ? err.message : 'Internal server error', - code: 'INTERNAL_ERROR', - stack: isDevelopment ? err.stack : undefined - }); -}; + // Default error response + res.status(500).json({ + error: isDevelopment ? err.message : 'Internal server error', + code: 'INTERNAL_ERROR', + stack: isDevelopment ? err.stack : undefined + }) +} export const notFoundHandler = (req, res) => { - logger.warn('Route not found', { - url: req.url, - method: req.method, - ip: req.ip - }); + logger.warn('Route not found', { + url: req.url, + method: req.method, + ip: req.ip + }) - res.status(404).json({ - error: 'Route not found', - code: 'ROUTE_NOT_FOUND' - }); -}; + res.status(404).json({ + error: 'Route not found', + code: 'ROUTE_NOT_FOUND' + }) +} // Async error wrapper export const asyncHandler = (fn) => { - return (req, res, next) => { - Promise.resolve(fn(req, res, next)).catch(next); - }; -}; + return (req, res, next) => { + Promise.resolve(fn(req, res, next)).catch(next) + } +} diff --git a/src/middleware/rateLimiter.js b/src/middleware/rateLimiter.js index 08724db..2aa2334 100644 --- a/src/middleware/rateLimiter.js +++ b/src/middleware/rateLimiter.js @@ -1,19 +1,19 @@ -import rateLimit from 'express-rate-limit'; -import logger from '../config/logger.js'; +import rateLimit from 'express-rate-limit' +import logger from '../config/logger.js' // Custom key generator for Cloudflare environments const generateKey = (req) => { - // In production with Cloudflare, prioritize CF-Connecting-IP - if (process.env.NODE_ENV === 'production') { - return req.headers['cf-connecting-ip'] || req.ip; - } - return req.ip; -}; + // In production with Cloudflare, prioritize CF-Connecting-IP + if (process.env.NODE_ENV === 'production') { + return req.headers['cf-connecting-ip'] || req.ip + } + return req.ip +} // Get the real client IP for logging const getClientIp = (req) => { - return req.headers['cf-connecting-ip'] || req.ip; -}; + return req.headers['cf-connecting-ip'] || req.ip +} // General rate limiter for all routes export const globalLimiter = rateLimit({ @@ -27,22 +27,22 @@ export const globalLimiter = rateLimit({ }, standardHeaders: true, legacyHeaders: false, handler: (req, res) => { - const retryAfter = Math.ceil(req.rateLimit.resetTime / 1000 - Date.now() / 1000); + const retryAfter = Math.ceil(req.rateLimit.resetTime / 1000 - Date.now() / 1000) logger.warn('Rate limit exceeded', { ip: getClientIp(req), userAgent: req.headers['user-agent'], path: req.path, retryAfter - }); + }) res.status(429).json({ error: 'Too many requests', message: 'Too many requests, please try again after 15 minutes', retryAfter: retryAfter, limitType: 'general' - }); + }) } -}); +}) // Stricter rate limiter for authentication routes export const authLimiter = rateLimit({ @@ -57,22 +57,22 @@ export const authLimiter = rateLimit({ }, standardHeaders: true, legacyHeaders: false, handler: (req, res) => { - const retryAfter = Math.ceil(req.rateLimit.resetTime / 1000 - Date.now() / 1000); + const retryAfter = Math.ceil(req.rateLimit.resetTime / 1000 - Date.now() / 1000) logger.warn('Auth rate limit exceeded', { ip: getClientIp(req), userAgent: req.headers['user-agent'], path: req.path, retryAfter - }); + }) res.status(429).json({ error: 'Too many login attempts', message: 'Too many login attempts, please try again after an hour', retryAfter: retryAfter, limitType: 'authentication' - }); + }) } -}); +}) // API rate limiter export const apiLimiter = rateLimit({ @@ -86,22 +86,22 @@ export const apiLimiter = rateLimit({ }, standardHeaders: true, legacyHeaders: false, handler: (req, res) => { - const retryAfter = Math.ceil(req.rateLimit.resetTime / 1000 - Date.now() / 1000); + const retryAfter = Math.ceil(req.rateLimit.resetTime / 1000 - Date.now() / 1000) logger.warn('API rate limit exceeded', { ip: getClientIp(req), userAgent: req.headers['user-agent'], path: req.path, retryAfter - }); + }) res.status(429).json({ error: 'Too many requests', message: 'Too many API requests, please try again after 15 minutes', retryAfter: retryAfter, limitType: 'API' - }); + }) } -}); +}) // Upload rate limiter (stricter for file uploads) export const uploadLimiter = rateLimit({ @@ -115,22 +115,22 @@ export const uploadLimiter = rateLimit({ }, standardHeaders: true, legacyHeaders: false, handler: (req, res) => { - const retryAfter = Math.ceil(req.rateLimit.resetTime / 1000 - Date.now() / 1000); + const retryAfter = Math.ceil(req.rateLimit.resetTime / 1000 - Date.now() / 1000) logger.warn('Upload rate limit exceeded', { ip: getClientIp(req), userAgent: req.headers['user-agent'], path: req.path, retryAfter - }); + }) res.status(429).json({ error: 'Too many upload requests', message: 'Too many upload requests, please try again after 15 minutes', retryAfter: retryAfter, limitType: 'upload' - }); + }) } -}); +}) // File access rate limiter export const fileLimiter = rateLimit({ @@ -144,22 +144,22 @@ export const fileLimiter = rateLimit({ }, standardHeaders: true, legacyHeaders: false, handler: (req, res) => { - const retryAfter = Math.ceil(req.rateLimit.resetTime / 1000 - Date.now() / 1000); + const retryAfter = Math.ceil(req.rateLimit.resetTime / 1000 - Date.now() / 1000) logger.warn('File access rate limit exceeded', { ip: getClientIp(req), userAgent: req.headers['user-agent'], path: req.path, retryAfter - }); + }) res.status(429).json({ error: 'Too many file requests', message: 'Too many file requests, please try again after 5 minutes', retryAfter: retryAfter, limitType: 'file' - }); + }) } -}); +}) // Strict upload rate limiter for chunked uploads (more frequent requests expected) export const strictUploadLimiter = rateLimit({ @@ -173,7 +173,7 @@ export const strictUploadLimiter = rateLimit({ }, standardHeaders: true, legacyHeaders: false, handler: (req, res) => { - const retryAfter = Math.ceil(req.rateLimit.resetTime / 1000 - Date.now() / 1000); + const retryAfter = Math.ceil(req.rateLimit.resetTime / 1000 - Date.now() / 1000) logger.warn('Strict upload rate limit exceeded', { ip: getClientIp(req), userAgent: req.headers['user-agent'], @@ -181,13 +181,13 @@ export const strictUploadLimiter = rateLimit({ sessionId: req.params.sessionId, chunkIndex: req.params.chunkIndex, retryAfter - }); + }) res.status(429).json({ error: 'Too many chunk upload requests', message: 'Too many chunk upload requests, please slow down', retryAfter: retryAfter, limitType: 'strictUpload' - }); + }) } -}); +}) diff --git a/src/middleware/requestLogger.js b/src/middleware/requestLogger.js index 41a09a9..fcd14b2 100644 --- a/src/middleware/requestLogger.js +++ b/src/middleware/requestLogger.js @@ -1,33 +1,33 @@ -import logger from '../config/logger.js'; +import logger from '../config/logger.js' export const requestLogger = (req, res, next) => { - const start = Date.now(); + const start = Date.now() - // Log request - logger.info('Request received', { - method: req.method, - url: req.url, - ip: req.ip, - userAgent: req.get('User-Agent'), - contentType: req.get('Content-Type'), - contentLength: req.get('Content-Length') - }); + // Log request + logger.info('Request received', { + method: req.method, + url: req.url, + ip: req.ip, + userAgent: req.get('User-Agent'), + contentType: req.get('Content-Type'), + contentLength: req.get('Content-Length') + }) - // Override res.end to log response - const originalEnd = res.end; - res.end = function(chunk, encoding) { - const duration = Date.now() - start; + // Override res.end to log response + const originalEnd = res.end + res.end = function(chunk, encoding) { + const duration = Date.now() - start - logger.info('Request completed', { - method: req.method, - url: req.url, - statusCode: res.statusCode, - duration: `${duration}ms`, - ip: req.ip - }); + logger.info('Request completed', { + method: req.method, + url: req.url, + statusCode: res.statusCode, + duration: `${duration}ms`, + ip: req.ip + }) - originalEnd.call(this, chunk, encoding); - }; + originalEnd.call(this, chunk, encoding) + } - next(); -}; + next() +} diff --git a/src/middleware/upload.js b/src/middleware/upload.js index 163eb6b..8ed6877 100644 --- a/src/middleware/upload.js +++ b/src/middleware/upload.js @@ -1,256 +1,256 @@ -import multer from 'multer'; -import path from 'path'; -import fs from 'fs'; -import { fileURLToPath } from 'url'; -import { fileTypeFromBuffer } from 'file-type'; -import crypto from 'crypto'; -import logger from '../config/logger.js'; +import multer from 'multer' +import path from 'path' +import fs from 'fs' +import { fileURLToPath } from 'url' +import { fileTypeFromBuffer } from 'file-type' +import crypto from 'crypto' +import logger from '../config/logger.js' -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) // Ensure upload directory exists -const uploadDir = process.env.UPLOAD_DIR || path.join(__dirname, '../../uploads'); +const uploadDir = process.env.UPLOAD_DIR || path.join(__dirname, '../../uploads') if (!fs.existsSync(uploadDir)) { - fs.mkdirSync(uploadDir, { recursive: true }); + fs.mkdirSync(uploadDir, { recursive: true }) } // File size limit from environment or default to 10MB -const maxFileSize = parseInt(process.env.MAX_FILE_SIZE) || 10 * 1024 * 1024; +const maxFileSize = parseInt(process.env.MAX_FILE_SIZE) || 10 * 1024 * 1024 // Allowed MIME types from environment const allowedMimeTypes = process.env.ALLOWED_MIME_TYPES - ? process.env.ALLOWED_MIME_TYPES.split(',').map(type => type.trim()) - : [ - 'image/jpeg', - 'image/png', - 'image/gif', - 'image/webp', - 'application/pdf', - 'text/plain', - 'application/msword', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' - ]; + ? process.env.ALLOWED_MIME_TYPES.split(',').map(type => type.trim()) + : [ + 'image/jpeg', + 'image/png', + 'image/gif', + 'image/webp', + 'application/pdf', + 'text/plain', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + ] // Dangerous file extensions to block const dangerousExtensions = [ - '.exe', '.bat', '.cmd', '.com', '.pif', '.scr', '.vbs', '.js', '.jar', - '.sh', '.ps1', '.php', '.asp', '.aspx', '.jsp', '.py', '.rb', '.pl' -]; + '.exe', '.bat', '.cmd', '.com', '.pif', '.scr', '.vbs', '.js', '.jar', + '.sh', '.ps1', '.php', '.asp', '.aspx', '.jsp', '.py', '.rb', '.pl' +] // Create storage configuration const storage = multer.diskStorage({ - destination: (req, file, cb) => { + destination: (req, file, cb) => { // Create context-specific subdirectories - const context = req.body.context || req.uploadToken?.context || 'general'; - const contextDir = path.join(uploadDir, context); + const context = req.body.context || req.uploadToken?.context || 'general' + const contextDir = path.join(uploadDir, context) - if (!fs.existsSync(contextDir)) { - fs.mkdirSync(contextDir, { recursive: true }); - } + if (!fs.existsSync(contextDir)) { + fs.mkdirSync(contextDir, { recursive: true }) + } - cb(null, contextDir); - }, filename: (req, file, cb) => { + cb(null, contextDir) + }, filename: (req, file, cb) => { // Generate clean filename with UUID and extension only - const fileId = crypto.randomUUID(); - const extension = path.extname(file.originalname).toLowerCase(); + const fileId = crypto.randomUUID() + const extension = path.extname(file.originalname).toLowerCase() - const filename = `${fileId}${extension}`; - cb(null, filename); - } -}); + const filename = `${fileId}${extension}` + cb(null, filename) + } +}) // File filter function const fileFilter = async (req, file, cb) => { - try { + try { // Check file extension - const extension = path.extname(file.originalname).toLowerCase(); + const extension = path.extname(file.originalname).toLowerCase() - if (dangerousExtensions.includes(extension)) { - logger.warn('Blocked dangerous file extension', { - filename: file.originalname, - extension, - ip: req.ip - }); - return cb(new Error(`File type ${extension} is not allowed for security reasons`)); - } + if (dangerousExtensions.includes(extension)) { + logger.warn('Blocked dangerous file extension', { + filename: file.originalname, + extension, + ip: req.ip + }) + return cb(new Error(`File type ${extension} is not allowed for security reasons`)) + } - // Check MIME type - if (!allowedMimeTypes.includes(file.mimetype)) { - logger.warn('Blocked disallowed MIME type', { - filename: file.originalname, - mimetype: file.mimetype, - ip: req.ip - }); - return cb(new Error(`File type ${file.mimetype} is not allowed`)); - } + // Check MIME type + if (!allowedMimeTypes.includes(file.mimetype)) { + logger.warn('Blocked disallowed MIME type', { + filename: file.originalname, + mimetype: file.mimetype, + ip: req.ip + }) + return cb(new Error(`File type ${file.mimetype} is not allowed`)) + } - cb(null, true); - } catch (error) { - logger.error('File filter error', { error: error.message }); - cb(error); - } -}; + cb(null, true) + } catch (error) { + logger.error('File filter error', { error: error.message }) + cb(error) + } +} // Create multer upload middleware const upload = multer({ - storage, - fileFilter, - limits: { - fileSize: maxFileSize, - files: 5, // Maximum 5 files per request - fields: 10, - fieldNameSize: 100, - fieldSize: 1024 * 1024 // 1MB for form fields - } -}); + storage, + fileFilter, + limits: { + fileSize: maxFileSize, + files: 5, // Maximum 5 files per request + fields: 10, + fieldNameSize: 100, + fieldSize: 1024 * 1024 // 1MB for form fields + } +}) // File validation middleware to verify file contents export const validateFileContents = async (req, res, next) => { - if (!req.files || req.files.length === 0) { - return next(); - } + if (!req.files || req.files.length === 0) { + return next() + } - try { - for (const file of req.files) { - // Read first few bytes to detect actual file type - const buffer = fs.readFileSync(file.path, { start: 0, end: 4095 }); - const detectedType = await fileTypeFromBuffer(buffer); + try { + for (const file of req.files) { + // Read first few bytes to detect actual file type + const buffer = fs.readFileSync(file.path, { start: 0, end: 4095 }) + const detectedType = await fileTypeFromBuffer(buffer) - if (detectedType) { - // Check if detected MIME type matches declared MIME type - if (detectedType.mime !== file.mimetype) { - logger.warn('MIME type mismatch detected', { - filename: file.originalname, - declared: file.mimetype, - detected: detectedType.mime, - ip: req.ip - }); + if (detectedType) { + // Check if detected MIME type matches declared MIME type + if (detectedType.mime !== file.mimetype) { + logger.warn('MIME type mismatch detected', { + filename: file.originalname, + declared: file.mimetype, + detected: detectedType.mime, + ip: req.ip + }) - // For security, we'll be strict about MIME type matching - if (!allowedMimeTypes.includes(detectedType.mime)) { - // Clean up uploaded file - fs.unlinkSync(file.path); - return res.status(400).json({ - error: `File appears to be ${detectedType.mime} but was declared as ${file.mimetype}`, - code: 'MIME_TYPE_MISMATCH' - }); - } + // For security, we'll be strict about MIME type matching + if (!allowedMimeTypes.includes(detectedType.mime)) { + // Clean up uploaded file + fs.unlinkSync(file.path) + return res.status(400).json({ + error: `File appears to be ${detectedType.mime} but was declared as ${file.mimetype}`, + code: 'MIME_TYPE_MISMATCH' + }) + } - // Update the MIME type to the detected one - file.mimetype = detectedType.mime; - } - } + // Update the MIME type to the detected one + file.mimetype = detectedType.mime + } + } - // Additional checks for images - if (file.mimetype.startsWith('image/')) { - // Basic image validation - check for valid image headers - const isValidImage = await validateImageFile(file.path); - if (!isValidImage) { - fs.unlinkSync(file.path); - return res.status(400).json({ - error: 'Invalid or corrupted image file', - code: 'INVALID_IMAGE' - }); + // Additional checks for images + if (file.mimetype.startsWith('image/')) { + // Basic image validation - check for valid image headers + const isValidImage = await validateImageFile(file.path) + if (!isValidImage) { + fs.unlinkSync(file.path) + return res.status(400).json({ + error: 'Invalid or corrupted image file', + code: 'INVALID_IMAGE' + }) + } + } } - } - } - next(); - } catch (error) { - logger.error('File content validation error', { error: error.message }); + next() + } catch (error) { + logger.error('File content validation error', { error: error.message }) - // Clean up any uploaded files on error - if (req.files) { - req.files.forEach(file => { - if (fs.existsSync(file.path)) { - fs.unlinkSync(file.path); + // Clean up any uploaded files on error + if (req.files) { + req.files.forEach(file => { + if (fs.existsSync(file.path)) { + fs.unlinkSync(file.path) + } + }) } - }); - } - res.status(500).json({ - error: 'File validation failed', - code: 'VALIDATION_ERROR' - }); - } -}; + res.status(500).json({ + error: 'File validation failed', + code: 'VALIDATION_ERROR' + }) + } +} // Basic image validation async function validateImageFile(filePath) { - try { - const buffer = fs.readFileSync(filePath, { start: 0, end: 1023 }); + try { + const buffer = fs.readFileSync(filePath, { start: 0, end: 1023 }) - // Check for common image file signatures - const signatures = { - jpeg: [0xFF, 0xD8, 0xFF], - png: [0x89, 0x50, 0x4E, 0x47], - gif: [0x47, 0x49, 0x46], - webp: [0x52, 0x49, 0x46, 0x46] // RIFF header for WebP - }; + // Check for common image file signatures + const signatures = { + jpeg: [0xFF, 0xD8, 0xFF], + png: [0x89, 0x50, 0x4E, 0x47], + gif: [0x47, 0x49, 0x46], + webp: [0x52, 0x49, 0x46, 0x46] // RIFF header for WebP + } + // eslint-disable-next-line no-unused-vars + for (const [format, signature] of Object.entries(signatures)) { + if (signature.every((byte, index) => buffer[index] === byte)) { + return true + } + } - for (const [format, signature] of Object.entries(signatures)) { - if (signature.every((byte, index) => buffer[index] === byte)) { - return true; - } + return false + } catch (error) { + logger.error('Image validation error', { error: error.message, filePath }) + return false } - - return false; - } catch (error) { - logger.error('Image validation error', { error: error.message, filePath }); - return false; - } } // Error handler for multer export const handleUploadError = (err, req, res, next) => { - if (err instanceof multer.MulterError) { - logger.warn('Multer upload error', { - error: err.message, - code: err.code, - ip: req.ip - }); + if (err instanceof multer.MulterError) { + logger.warn('Multer upload error', { + error: err.message, + code: err.code, + ip: req.ip + }) - switch (err.code) { - case 'LIMIT_FILE_SIZE': - return res.status(400).json({ - error: `File too large. Maximum size is ${Math.round(maxFileSize / 1024 / 1024)}MB`, - code: 'FILE_TOO_LARGE' - }); - case 'LIMIT_FILE_COUNT': - return res.status(400).json({ - error: 'Too many files. Maximum 5 files per request', - code: 'TOO_MANY_FILES' - }); - case 'LIMIT_UNEXPECTED_FILE': - return res.status(400).json({ - error: 'Unexpected file field', - code: 'UNEXPECTED_FILE' - }); - default: - return res.status(400).json({ - error: err.message, - code: 'UPLOAD_ERROR' - }); + switch (err.code) { + case 'LIMIT_FILE_SIZE': + return res.status(400).json({ + error: `File too large. Maximum size is ${Math.round(maxFileSize / 1024 / 1024)}MB`, + code: 'FILE_TOO_LARGE' + }) + case 'LIMIT_FILE_COUNT': + return res.status(400).json({ + error: 'Too many files. Maximum 5 files per request', + code: 'TOO_MANY_FILES' + }) + case 'LIMIT_UNEXPECTED_FILE': + return res.status(400).json({ + error: 'Unexpected file field', + code: 'UNEXPECTED_FILE' + }) + default: + return res.status(400).json({ + error: err.message, + code: 'UPLOAD_ERROR' + }) + } } - } - if (err.message.includes('not allowed')) { - return res.status(400).json({ - error: err.message, - code: 'FILE_TYPE_NOT_ALLOWED' - }); - } + if (err.message.includes('not allowed')) { + return res.status(400).json({ + error: err.message, + code: 'FILE_TYPE_NOT_ALLOWED' + }) + } - next(err); -}; + next(err) +} // Export configured upload middleware -export const uploadSingle = upload.single('file'); -export const uploadMultiple = upload.array('files', 5); +export const uploadSingle = upload.single('file') +export const uploadMultiple = upload.array('files', 5) export const uploadFields = upload.fields([ - { name: 'files', maxCount: 5 }, - { name: 'file', maxCount: 1 } -]); + { name: 'files', maxCount: 5 }, + { name: 'file', maxCount: 1 } +]) -export { allowedMimeTypes, maxFileSize, uploadDir }; +export { allowedMimeTypes, maxFileSize, uploadDir } diff --git a/src/models/File.js b/src/models/File.js index 1ce775d..ff102bb 100644 --- a/src/models/File.js +++ b/src/models/File.js @@ -1,5 +1,5 @@ -import database from '../config/database.js'; -import logger from '../config/logger.js'; +import database from '../config/database.js' +import logger from '../config/logger.js' class FileModel { async create(fileData) { const query = ` @@ -9,171 +9,171 @@ class FileModel { async create(fileData) { metadata, is_public, file_hash, created_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, NOW()) RETURNING * - `; + ` const values = [ - fileData.id, - fileData.filename, - fileData.originalName, - fileData.filePath, - fileData.mimeType, - fileData.fileSize, - fileData.uploadContext || 'general', - fileData.uploadedBy || 'system', - fileData.uploadSource || 'api', - JSON.stringify(fileData.metadata || {}), - fileData.isPublic || false, - fileData.fileHash || null, - ]; + fileData.id, + fileData.filename, + fileData.originalName, + fileData.filePath, + fileData.mimeType, + fileData.fileSize, + fileData.uploadContext || 'general', + fileData.uploadedBy || 'system', + fileData.uploadSource || 'api', + JSON.stringify(fileData.metadata || {}), + fileData.isPublic || false, + fileData.fileHash || null, + ] try { - const result = await database.query(query, values); - logger.info('File record created', { fileId: fileData.id }); - return result.rows[0]; + const result = await database.query(query, values) + logger.info('File record created', { fileId: fileData.id }) + return result.rows[0] } catch (error) { - logger.error('Failed to create file record', { - error: error.message, - fileId: fileData.id - }); - throw error; + logger.error('Failed to create file record', { + error: error.message, + fileId: fileData.id + }) + throw error } - } - async findById(id) { - const query = 'SELECT * FROM files WHERE id = $1 AND deleted_at IS NULL'; +} +async findById(id) { + const query = 'SELECT * FROM files WHERE id = $1 AND deleted_at IS NULL' try { - const result = await database.query(query, [id]); - return result.rows[0] || null; + const result = await database.query(query, [id]) + return result.rows[0] || null } catch (error) { - logger.error('Failed to find file by ID', { error: error.message, id }); - throw error; + logger.error('Failed to find file by ID', { error: error.message, id }) + throw error } - } +} - async findByHash(hash) { - const query = 'SELECT * FROM files WHERE file_hash = $1 AND deleted_at IS NULL'; +async findByHash(hash) { + const query = 'SELECT * FROM files WHERE file_hash = $1 AND deleted_at IS NULL' try { - const result = await database.query(query, [hash]); - return result.rows[0] || null; + const result = await database.query(query, [hash]) + return result.rows[0] || null } catch (error) { - logger.error('Failed to find file by hash', { error: error.message, hash }); - throw error; + logger.error('Failed to find file by hash', { error: error.message, hash }) + throw error } - } +} - async findDuplicatesByHash(hash) { - const query = 'SELECT * FROM files WHERE file_hash = $1 AND deleted_at IS NULL ORDER BY created_at ASC'; +async findDuplicatesByHash(hash) { + const query = 'SELECT * FROM files WHERE file_hash = $1 AND deleted_at IS NULL ORDER BY created_at ASC' try { - const result = await database.query(query, [hash]); - return result.rows; + const result = await database.query(query, [hash]) + return result.rows } catch (error) { - logger.error('Failed to find duplicates by hash', { error: error.message, hash }); - throw error; + logger.error('Failed to find duplicates by hash', { error: error.message, hash }) + throw error } - } +} - async findByContext(context, limit = 50, offset = 0) { +async findByContext(context, limit = 50, offset = 0) { const query = ` SELECT * FROM files WHERE upload_context = $1 AND deleted_at IS NULL ORDER BY created_at DESC LIMIT $2 OFFSET $3 - `; + ` try { - const result = await database.query(query, [context, limit, offset]); - return result.rows; + const result = await database.query(query, [context, limit, offset]) + return result.rows } catch (error) { - logger.error('Failed to find files by context', { - error: error.message, - context - }); - throw error; + logger.error('Failed to find files by context', { + error: error.message, + context + }) + throw error } - } - async findByUploadedBy(uploadedBy, limit = 50, offset = 0) { +} +async findByUploadedBy(uploadedBy, limit = 50, offset = 0) { const query = ` SELECT * FROM files WHERE uploaded_by = $1 AND deleted_at IS NULL ORDER BY created_at DESC LIMIT $2 OFFSET $3 - `; + ` try { - const result = await database.query(query, [uploadedBy, limit, offset]); - return result.rows; + const result = await database.query(query, [uploadedBy, limit, offset]) + return result.rows } catch (error) { - logger.error('Failed to find files by uploader', { - error: error.message, - uploadedBy - }); - throw error; + logger.error('Failed to find files by uploader', { + error: error.message, + uploadedBy + }) + throw error } - } +} - async findByContextAndFilename(context, filename) { +async findByContextAndFilename(context, filename) { const query = ` SELECT * FROM files WHERE upload_context = $1 AND original_name = $2 AND deleted_at IS NULL ORDER BY created_at DESC LIMIT 1 - `; + ` try { - const result = await database.query(query, [context, filename]); - return result.rows[0] || null; + const result = await database.query(query, [context, filename]) + return result.rows[0] || null } catch (error) { - logger.error('Failed to find file by context and filename', { - error: error.message, - context, - filename - }); - throw error; + logger.error('Failed to find file by context and filename', { + error: error.message, + context, + filename + }) + throw error } - } +} - async findPublicByContextAndFilename(context, filename) { +async findPublicByContextAndFilename(context, filename) { const query = ` SELECT * FROM files WHERE upload_context = $1 AND original_name = $2 AND is_public = TRUE AND deleted_at IS NULL ORDER BY created_at DESC LIMIT 1 - `; + ` try { - const result = await database.query(query, [context, filename]); - return result.rows[0] || null; + const result = await database.query(query, [context, filename]) + return result.rows[0] || null } catch (error) { - logger.error('Failed to find public file by context and filename', { - error: error.message, - context, - filename - }); - throw error; + logger.error('Failed to find public file by context and filename', { + error: error.message, + context, + filename + }) + throw error } - } +} - async findPublicById(id) { - const query = 'SELECT * FROM files WHERE id = $1 AND is_public = TRUE AND deleted_at IS NULL'; +async findPublicById(id) { + const query = 'SELECT * FROM files WHERE id = $1 AND is_public = TRUE AND deleted_at IS NULL' try { - const result = await database.query(query, [id]); - return result.rows[0] || null; + const result = await database.query(query, [id]) + return result.rows[0] || null } catch (error) { - logger.error('Failed to find public file by ID', { error: error.message, id }); - throw error; + logger.error('Failed to find public file by ID', { error: error.message, id }) + throw error } - } - /** +} +/** * Find public file by hash prefix and context * @param {string} hashPrefix - First 12 characters of file hash * @param {string} context - Upload context * @returns {Object|null} File record */ - async findPublicByHashAndContext(hashPrefix, context) { +async findPublicByHashAndContext(hashPrefix, context) { const query = ` SELECT * FROM files WHERE LEFT(file_hash, 12) = $1 @@ -182,60 +182,60 @@ class FileModel { async create(fileData) { AND deleted_at IS NULL ORDER BY created_at DESC LIMIT 1 - `; + ` try { - const result = await database.query(query, [hashPrefix, context]); - return result.rows[0] || null; + const result = await database.query(query, [hashPrefix, context]) + return result.rows[0] || null } catch (error) { - logger.error('Failed to find public file by hash and context', { - error: error.message, - hashPrefix, - context - }); - throw error; + logger.error('Failed to find public file by hash and context', { + error: error.message, + hashPrefix, + context + }) + throw error } - } +} - async updateAccessCount(id) { +async updateAccessCount(id) { const query = ` UPDATE files SET access_count = access_count + 1, last_accessed_at = NOW() WHERE id = $1 AND deleted_at IS NULL RETURNING access_count - `; + ` try { - const result = await database.query(query, [id]); - return result.rows[0]?.access_count || 0; + const result = await database.query(query, [id]) + return result.rows[0]?.access_count || 0 } catch (error) { - logger.error('Failed to update access count', { error: error.message, id }); - throw error; + logger.error('Failed to update access count', { error: error.message, id }) + throw error } - } +} - async softDelete(id) { +async softDelete(id) { const query = ` UPDATE files SET deleted_at = NOW() WHERE id = $1 AND deleted_at IS NULL RETURNING * - `; + ` try { - const result = await database.query(query, [id]); - if (result.rows.length > 0) { - logger.info('File soft deleted', { fileId: id }); - return result.rows[0]; - } - return null; + const result = await database.query(query, [id]) + if (result.rows.length > 0) { + logger.info('File soft deleted', { fileId: id }) + return result.rows[0] + } + return null } catch (error) { - logger.error('Failed to soft delete file', { error: error.message, id }); - throw error; + logger.error('Failed to soft delete file', { error: error.message, id }) + throw error } - } +} - async getStats() { +async getStats() { const query = ` SELECT COUNT(*) as total_files, @@ -245,34 +245,34 @@ class FileModel { async create(fileData) { COUNT(DISTINCT uploaded_by) as unique_uploaders FROM files WHERE deleted_at IS NULL - `; + ` try { - const result = await database.query(query); - return result.rows[0]; + const result = await database.query(query) + return result.rows[0] } catch (error) { - logger.error('Failed to get file stats', { error: error.message }); - throw error; + logger.error('Failed to get file stats', { error: error.message }) + throw error } - } +} - async cleanupOldFiles(daysOld = 90) { +async cleanupOldFiles(daysOld = 90) { const query = ` DELETE FROM files WHERE deleted_at IS NOT NULL AND deleted_at < NOW() - INTERVAL '${daysOld} days' RETURNING id - `; + ` try { - const result = await database.query(query); - logger.info('Cleaned up old files', { count: result.rows.length }); - return result.rows; + const result = await database.query(query) + logger.info('Cleaned up old files', { count: result.rows.length }) + return result.rows } catch (error) { - logger.error('Failed to cleanup old files', { error: error.message }); - throw error; + logger.error('Failed to cleanup old files', { error: error.message }) + throw error } - } +} } -export default new FileModel(); +export default new FileModel() diff --git a/src/routes/chunkedUpload.js b/src/routes/chunkedUpload.js index 1fab825..5fde15b 100644 --- a/src/routes/chunkedUpload.js +++ b/src/routes/chunkedUpload.js @@ -1,22 +1,22 @@ -import express from 'express'; -import ChunkedUploadController from '../controllers/ChunkedUploadController.js'; -import { authenticate } from '../middleware/auth.js'; -import { uploadLimiter, strictUploadLimiter } from '../middleware/rateLimiter.js'; +import express from 'express' +import ChunkedUploadController from '../controllers/ChunkedUploadController.js' +import { authenticate } from '../middleware/auth.js' +import { uploadLimiter, strictUploadLimiter } from '../middleware/rateLimiter.js' import { - uploadSingleChunk, - handleChunkUploadError, - validateChunkParams, - validateSessionInit -} from '../middleware/chunkUpload.js'; + uploadSingleChunk, + handleChunkUploadError, + validateChunkParams, + validateSessionInit +} from '../middleware/chunkUpload.js' -const router = express.Router(); +const router = express.Router() /** * GET /api/upload/chunk/config * Get chunked upload configuration * No authentication required for config endpoint */ -router.get('/config', ChunkedUploadController.getConfig); +router.get('/config', ChunkedUploadController.getConfig) /** * POST /api/upload/chunk/init @@ -24,41 +24,41 @@ router.get('/config', ChunkedUploadController.getConfig); * Supports same authentication methods as regular upload */ router.post('/init', - uploadLimiter, - authenticate, - validateSessionInit, - ChunkedUploadController.initializeUpload -); + uploadLimiter, + authenticate, + validateSessionInit, + ChunkedUploadController.initializeUpload +) /** * GET /api/upload/chunk/:sessionId/status * Get upload session status and missing chunks */ router.get('/:sessionId/status', - uploadLimiter, - authenticate, - ChunkedUploadController.getSessionStatus -); + uploadLimiter, + authenticate, + ChunkedUploadController.getSessionStatus +) /** * POST /api/upload/chunk/:sessionId/complete * Complete chunked upload (assemble final file) */ router.post('/:sessionId/complete', - uploadLimiter, - authenticate, - ChunkedUploadController.completeUpload -); + uploadLimiter, + authenticate, + ChunkedUploadController.completeUpload +) /** * DELETE /api/upload/chunk/:sessionId * Cancel upload session and cleanup */ router.delete('/:sessionId', - uploadLimiter, - authenticate, - ChunkedUploadController.cancelUpload -); + uploadLimiter, + authenticate, + ChunkedUploadController.cancelUpload +) /** * POST /api/upload/chunk/:sessionId/:chunkIndex @@ -67,12 +67,12 @@ router.delete('/:sessionId', * Note: This route must come AFTER the specific routes above */ router.post('/:sessionId/:chunkIndex', - strictUploadLimiter, - authenticate, - validateChunkParams, - uploadSingleChunk, - handleChunkUploadError, - ChunkedUploadController.uploadChunk -); + strictUploadLimiter, + authenticate, + validateChunkParams, + uploadSingleChunk, + handleChunkUploadError, + ChunkedUploadController.uploadChunk +) -export default router; +export default router diff --git a/src/routes/files.js b/src/routes/files.js index 6e0a6dc..5a97bb3 100644 --- a/src/routes/files.js +++ b/src/routes/files.js @@ -1,40 +1,41 @@ -import express from 'express'; -import FileController from '../controllers/FileController.js'; -import { authenticate, authenticateService } from '../middleware/auth.js'; +import express from 'express' +import FileController from '../controllers/FileController.js' +// eslint-disable-next-line no-unused-vars +import { authenticate, authenticateService } from '../middleware/auth.js' -const router = express.Router(); +const router = express.Router() /** * GET /files/uploader/:uploaderId * Get files by uploader ID * Users can only see their own files unless service authenticated */ -router.get('/uploader/:uploaderId', authenticate, FileController.getFilesByUploader); +router.get('/uploader/:uploaderId', authenticate, FileController.getFilesByUploader) /** * GET /files/:fileId/info * Get file information and metadata */ -router.get('/:fileId/info', authenticate, FileController.getFileInfo); +router.get('/:fileId/info', authenticate, FileController.getFileInfo) /** * DELETE /files/:fileId * Delete file (soft delete in DB, removes from disk) */ -router.delete('/:fileId', authenticate, FileController.deleteFile); +router.delete('/:fileId', authenticate, FileController.deleteFile) /** * POST /files/:fileId/signed-url * Generate signed URL for temporary file access */ -router.post('/:fileId/signed-url', authenticate, FileController.generateSignedUrl); +router.post('/:fileId/signed-url', authenticate, FileController.generateSignedUrl) /** * GET /files/:context/:filename * Serve file by context and filename (authenticated) * This route handles context/filename access */ -router.get('/:context/:filename', authenticate, FileController.serveFileByContextAndFilename); +router.get('/:context/:filename', authenticate, FileController.serveFileByContextAndFilename) /** * GET /files/:fileId @@ -42,6 +43,6 @@ router.get('/:context/:filename', authenticate, FileController.serveFileByContex * Supports token-based access via query parameter or authentication headers * This route is last to avoid conflicts with other patterns */ -router.get('/:fileId', authenticate, FileController.serveFile); +router.get('/:fileId', authenticate, FileController.serveFile) -export default router; +export default router diff --git a/src/routes/health.js b/src/routes/health.js index bf493fd..d0e7732 100644 --- a/src/routes/health.js +++ b/src/routes/health.js @@ -1,129 +1,130 @@ -import express from 'express'; -import database from '../config/database.js'; -import { asyncHandler } from '../middleware/errorHandler.js'; +import express from 'express' +import database from '../config/database.js' +import { asyncHandler } from '../middleware/errorHandler.js' -const router = express.Router(); +const router = express.Router() /** * GET /api/health * Basic health check endpoint */ router.get('/', asyncHandler(async (req, res) => { - const health = { - status: 'ok', - timestamp: new Date().toISOString(), - service: 'otto', - version: '1.0.0', - uptime: process.uptime(), - environment: process.env.NODE_ENV || 'development' - }; + const health = { + status: 'ok', + timestamp: new Date().toISOString(), + service: 'otto', + version: '1.0.0', + uptime: process.uptime(), + environment: process.env.NODE_ENV || 'development' + } - // Test database connection - try { - await database.testConnection(); - health.database = 'connected'; - } catch (error) { - health.status = 'warning'; - health.database = 'disconnected'; - health.databaseError = error.message; - } + // Test database connection + try { + await database.testConnection() + health.database = 'connected' + } catch (error) { + health.status = 'warning' + health.database = 'disconnected' + health.databaseError = error.message + } - // Check disk space for uploads directory - try { - const fs = await import('fs'); - const uploadDir = process.env.UPLOAD_DIR || './uploads'; - const stats = fs.statSync(uploadDir); - health.uploadsDirectory = 'accessible'; - } catch (error) { - health.status = 'warning'; - health.uploadsDirectory = 'inaccessible'; - health.uploadsError = error.message; - } + // Check disk space for uploads directory + try { + const fs = await import('fs') + const uploadDir = process.env.UPLOAD_DIR || './uploads' + // eslint-disable-next-line no-unused-vars + const stats = fs.statSync(uploadDir) + health.uploadsDirectory = 'accessible' + } catch (error) { + health.status = 'warning' + health.uploadsDirectory = 'inaccessible' + health.uploadsError = error.message + } - const statusCode = health.status === 'ok' ? 200 : 503; - res.status(statusCode).json(health); -})); + const statusCode = health.status === 'ok' ? 200 : 503 + res.status(statusCode).json(health) +})) /** * GET /api/health/detailed * Detailed health check with system information */ router.get('/detailed', asyncHandler(async (req, res) => { - const health = { - status: 'ok', - timestamp: new Date().toISOString(), - service: 'otto', - version: '1.0.0', - uptime: process.uptime(), - environment: process.env.NODE_ENV || 'development', - system: { - platform: process.platform, - nodeVersion: process.version, - memory: { - used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024), - total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024), - external: Math.round(process.memoryUsage().external / 1024 / 1024) - }, - cpu: process.cpuUsage() + const health = { + status: 'ok', + timestamp: new Date().toISOString(), + service: 'otto', + version: '1.0.0', + uptime: process.uptime(), + environment: process.env.NODE_ENV || 'development', + system: { + platform: process.platform, + nodeVersion: process.version, + memory: { + used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024), + total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024), + external: Math.round(process.memoryUsage().external / 1024 / 1024) + }, + cpu: process.cpuUsage() + } } - }; - // Test database connection with timing - try { - const start = Date.now(); - await database.testConnection(); - const duration = Date.now() - start; - health.database = { - status: 'connected', - responseTime: `${duration}ms` - }; - } catch (error) { - health.status = 'warning'; - health.database = { - status: 'disconnected', - error: error.message - }; - } + // Test database connection with timing + try { + const start = Date.now() + await database.testConnection() + const duration = Date.now() - start + health.database = { + status: 'connected', + responseTime: `${duration}ms` + } + } catch (error) { + health.status = 'warning' + health.database = { + status: 'disconnected', + error: error.message + } + } - // Check uploads directory with details - try { - const fs = await import('fs'); - const path = await import('path'); - const uploadDir = process.env.UPLOAD_DIR || './uploads'; - const stats = fs.statSync(uploadDir); + // Check uploads directory with details + try { + const fs = await import('fs') + const path = await import('path') + const uploadDir = process.env.UPLOAD_DIR || './uploads' + const stats = fs.statSync(uploadDir) - // Count files in upload directory - const countFiles = (dir) => { - let count = 0; - const items = fs.readdirSync(dir); - for (const item of items) { - const itemPath = path.join(dir, item); - const itemStats = fs.statSync(itemPath); - if (itemStats.isDirectory()) { - count += countFiles(itemPath); - } else { - count++; + // Count files in upload directory + const countFiles = (dir) => { + let count = 0 + const items = fs.readdirSync(dir) + for (const item of items) { + const itemPath = path.join(dir, item) + const itemStats = fs.statSync(itemPath) + if (itemStats.isDirectory()) { + count += countFiles(itemPath) + } else { + count++ + } + } + return count } - } - return count; - }; - health.uploadsDirectory = { - status: 'accessible', - path: uploadDir, - fileCount: countFiles(uploadDir), - created: stats.birthtime - }; - } catch (error) { - health.status = 'warning'; - health.uploadsDirectory = { - status: 'inaccessible', - error: error.message - }; - } + health.uploadsDirectory = { + status: 'accessible', + path: uploadDir, + fileCount: countFiles(uploadDir), + created: stats.birthtime + } + } catch (error) { + health.status = 'warning' + health.uploadsDirectory = { + status: 'inaccessible', + error: error.message + } + } - const statusCode = health.status === 'ok' ? 200 : 503; - res.status(statusCode).json(health); -})); + const statusCode = health.status === 'ok' ? 200 : 503 + res.status(statusCode).json(health) +})) -export default router; +export default router diff --git a/src/routes/public.js b/src/routes/public.js index e0275fe..6be3e38 100644 --- a/src/routes/public.js +++ b/src/routes/public.js @@ -1,7 +1,7 @@ -import express from 'express'; -import FileController from '../controllers/FileController.js'; +import express from 'express' +import FileController from '../controllers/FileController.js' -const router = express.Router(); +const router = express.Router() /** * GET /public/:context/:hash.:ext @@ -9,7 +9,7 @@ const router = express.Router(); * Content-addressable URLs prevent collisions * No authentication required */ -router.get('/:context/:hash.:ext', FileController.servePublicFileByHashWithExt); +router.get('/:context/:hash.:ext', FileController.servePublicFileByHashWithExt) /** * GET /public/:context/:hash @@ -17,21 +17,21 @@ router.get('/:context/:hash.:ext', FileController.servePublicFileByHashWithExt); * Content-addressable URLs prevent collisions * No authentication required */ -router.get('/:context/:hash', FileController.servePublicFileByHash); +router.get('/:context/:hash', FileController.servePublicFileByHash) /** * GET /p/:context/:hash.:ext * Short URL for public files with extension * No authentication required */ -router.get('/p/:context/:hash.:ext', FileController.servePublicFileByHashWithExt); +router.get('/p/:context/:hash.:ext', FileController.servePublicFileByHashWithExt) /** * GET /p/:context/:hash * Short URL for public files * No authentication required */ -router.get('/p/:context/:hash', FileController.servePublicFileByHash); +router.get('/p/:context/:hash', FileController.servePublicFileByHash) /** * LEGACY: GET /public/:context/:filename @@ -39,13 +39,13 @@ router.get('/p/:context/:hash', FileController.servePublicFileByHash); * Will be deprecated - returns latest file with that name * No authentication required */ -router.get('/:context/:filename', FileController.servePublicFileLegacy); +router.get('/:context/:filename', FileController.servePublicFileLegacy) /** * LEGACY: GET /p/:context/:filename * Short URL for public files (backward compatibility) * No authentication required */ -router.get('/p/:context/:filename', FileController.servePublicFileLegacy); +router.get('/p/:context/:filename', FileController.servePublicFileLegacy) -export default router; +export default router diff --git a/src/routes/upload.js b/src/routes/upload.js index faa1b54..a5b0555 100644 --- a/src/routes/upload.js +++ b/src/routes/upload.js @@ -1,17 +1,17 @@ -import express from 'express'; -import UploadController from '../controllers/UploadController.js'; -import { authenticate, authenticateService } from '../middleware/auth.js'; -import { uploadMultiple, validateFileContents, handleUploadError } from '../middleware/upload.js'; -import { uploadLimiter, authLimiter } from '../middleware/rateLimiter.js'; +import express from 'express' +import UploadController from '../controllers/UploadController.js' +import { authenticate, authenticateService } from '../middleware/auth.js' +import { uploadMultiple, validateFileContents, handleUploadError } from '../middleware/upload.js' +import { uploadLimiter, authLimiter } from '../middleware/rateLimiter.js' -const router = express.Router(); +const router = express.Router() /** * POST /api/upload/token * Generate upload token for frontend uploads * Requires service authentication */ -router.post('/token', authLimiter, authenticateService, UploadController.generateUploadToken); +router.post('/token', authLimiter, authenticateService, UploadController.generateUploadToken) /** * POST /api/upload @@ -21,26 +21,26 @@ router.post('/token', authLimiter, authenticateService, UploadController.generat * - JWT token for user uploads */ router.post('/', - uploadLimiter, - authenticate, - uploadMultiple, - handleUploadError, - validateFileContents, - UploadController.uploadFiles -); + uploadLimiter, + authenticate, + uploadMultiple, + handleUploadError, + validateFileContents, + UploadController.uploadFiles +) /** * GET /api/upload/stats * Get upload statistics * Requires service authentication */ -router.get('/stats', authenticateService, UploadController.getUploadStats); +router.get('/stats', authenticateService, UploadController.getUploadStats) /** * GET /api/upload/context/:context * Get files by upload context * Requires service authentication */ -router.get('/context/:context', authenticateService, UploadController.getFilesByContext); +router.get('/context/:context', authenticateService, UploadController.getFilesByContext) -export default router; +export default router diff --git a/src/scripts/migrate.js b/src/scripts/migrate.js index 7ad75e9..938fb4c 100644 --- a/src/scripts/migrate.js +++ b/src/scripts/migrate.js @@ -1,11 +1,11 @@ -import database from '../config/database.js'; -import logger from '../config/logger.js'; +import database from '../config/database.js' +import logger from '../config/logger.js' const migrations = [ - { - version: 1, - name: 'create_files_table', - up: ` + { + version: 1, + name: 'create_files_table', + up: ` CREATE TABLE IF NOT EXISTS files ( id UUID PRIMARY KEY, filename VARCHAR(255) NOT NULL, @@ -23,12 +23,12 @@ const migrations = [ deleted_at TIMESTAMP WITH TIME ZONE ); `, - down: `DROP TABLE IF EXISTS files;` - }, - { - version: 2, - name: 'create_indexes', - up: ` + down: 'DROP TABLE IF EXISTS files;' + }, + { + version: 2, + name: 'create_indexes', + up: ` CREATE INDEX IF NOT EXISTS idx_files_upload_context ON files(upload_context); CREATE INDEX IF NOT EXISTS idx_files_uploaded_by ON files(uploaded_by); CREATE INDEX IF NOT EXISTS idx_files_created_at ON files(created_at); @@ -41,195 +41,200 @@ const migrations = [ DROP INDEX IF EXISTS idx_files_deleted_at; DROP INDEX IF EXISTS idx_files_mime_type; ` - }, - { - version: 3, - name: 'create_migration_table', - up: ` + }, + { + version: 3, + name: 'create_migration_table', + up: ` CREATE TABLE IF NOT EXISTS migrations ( version INTEGER PRIMARY KEY, name VARCHAR(255) NOT NULL, executed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); `, - down: `DROP TABLE IF EXISTS migrations;` - }, { - version: 4, - name: 'add_public_support', - up: ` + down: 'DROP TABLE IF EXISTS migrations;' + }, { + version: 4, + name: 'add_public_support', + up: ` ALTER TABLE files ADD COLUMN IF NOT EXISTS is_public BOOLEAN DEFAULT FALSE; CREATE INDEX IF NOT EXISTS idx_files_is_public ON files(is_public); CREATE INDEX IF NOT EXISTS idx_files_context_filename ON files(upload_context, original_name) WHERE deleted_at IS NULL; `, - down: ` + down: ` DROP INDEX IF EXISTS idx_files_is_public; DROP INDEX IF EXISTS idx_files_context_filename; ALTER TABLE files DROP COLUMN IF EXISTS is_public; ` - }, - { - version: 5, - name: 'add_file_hash_support', - up: ` + }, + { + version: 5, + name: 'add_file_hash_support', + up: ` ALTER TABLE files ADD COLUMN IF NOT EXISTS file_hash VARCHAR(64); CREATE INDEX IF NOT EXISTS idx_files_hash ON files(file_hash) WHERE file_hash IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_files_hash_size ON files(file_hash, file_size) WHERE file_hash IS NOT NULL; `, - down: ` + down: ` DROP INDEX IF EXISTS idx_files_hash; DROP INDEX IF EXISTS idx_files_hash_size; ALTER TABLE files DROP COLUMN IF EXISTS file_hash; ` - } -]; + } +] class MigrationRunner { - async getCurrentVersion() { - try { - const result = await database.query( - 'SELECT MAX(version) as version FROM migrations' - ); - return result.rows[0]?.version || 0; - } catch (error) { - // Migration table doesn't exist yet - return 0; + async getCurrentVersion() { + try { + const result = await database.query( + 'SELECT MAX(version) as version FROM migrations' + ) + return result.rows[0]?.version || 0 + } catch { + // Migration table doesn't exist yet + return 0 + } } - } - async recordMigration(version, name) { - await database.query( - 'INSERT INTO migrations (version, name) VALUES ($1, $2)', - [version, name] - ); - } - async runMigrations() { - logger.info('Starting database migrations...'); + async recordMigration(version, name) { + await database.query( + 'INSERT INTO migrations (version, name) VALUES ($1, $2)', + [version, name] + ) + } + async runMigrations() { + logger.info('Starting database migrations...') - try { - const currentVersion = await this.getCurrentVersion(); - logger.info(`Current database version: ${currentVersion}`); + try { + const currentVersion = await this.getCurrentVersion() + logger.info(`Current database version: ${currentVersion}`) - const pendingMigrations = migrations.filter(m => m.version > currentVersion); + const pendingMigrations = migrations.filter(m => m.version > currentVersion) - if (pendingMigrations.length === 0) { - logger.info('No pending migrations'); - return; - } + if (pendingMigrations.length === 0) { + logger.info('No pending migrations') + return + } - logger.info(`Found ${pendingMigrations.length} pending migrations`); + logger.info(`Found ${pendingMigrations.length} pending migrations`) - for (const migration of pendingMigrations) { - logger.info(`Running migration ${migration.version}: ${migration.name}`); + for (const migration of pendingMigrations) { + logger.info(`Running migration ${migration.version}: ${migration.name}`) - await database.transaction(async (client) => { - // Run the migration - await client.query(migration.up); + await database.transaction(async (client) => { + // Run the migration + await client.query(migration.up) - // Record this migration in the migrations table - // First ensure the migrations table exists for version 3+ - if (migration.version >= 3) { - await client.query(` + // Record this migration in the migrations table + // First ensure the migrations table exists for version 3+ + if (migration.version >= 3) { + await client.query(` CREATE TABLE IF NOT EXISTS migrations ( version INTEGER PRIMARY KEY, name VARCHAR(255) NOT NULL, executed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); - `); - } + `) + } - await client.query( - 'INSERT INTO migrations (version, name) VALUES ($1, $2)', - [migration.version, migration.name] - ); - }); - - logger.info(`Migration ${migration.version} completed`); - } - - logger.info('All migrations completed successfully'); - } catch (error) { - logger.error('Migration failed', { error: error.message }); - throw error; + await client.query( + 'INSERT INTO migrations (version, name) VALUES ($1, $2)', + [migration.version, migration.name] + ) + }) + + logger.info(`Migration ${migration.version} completed`) + } + + logger.info('All migrations completed successfully') + } catch (error) { + logger.error('Migration failed', { error: error.message }) + throw error + } } - } - async rollback(targetVersion) { - logger.info(`Rolling back to version ${targetVersion}`); + async rollback(targetVersion) { + logger.info(`Rolling back to version ${targetVersion}`) - try { - const currentVersion = await this.getCurrentVersion(); + try { + const currentVersion = await this.getCurrentVersion() - if (currentVersion <= targetVersion) { - logger.info('No rollback needed'); - return; - } + if (currentVersion <= targetVersion) { + logger.info('No rollback needed') + return + } - const migrationsToRollback = migrations - .filter(m => m.version > targetVersion && m.version <= currentVersion) - .sort((a, b) => b.version - a.version); // Reverse order for rollback + const migrationsToRollback = migrations + .filter(m => m.version > targetVersion && m.version <= currentVersion) + .sort((a, b) => b.version - a.version) // Reverse order for rollback - for (const migration of migrationsToRollback) { - logger.info(`Rolling back migration ${migration.version}: ${migration.name}`); + for (const migration of migrationsToRollback) { + logger.info(`Rolling back migration ${migration.version}: ${migration.name}`) - await database.transaction(async (client) => { - // Run the rollback - await client.query(migration.down); + await database.transaction(async (client) => { + // Run the rollback + await client.query(migration.down) - // Remove migration record - await client.query( - 'DELETE FROM migrations WHERE version = $1', - [migration.version] - ); - }); - - logger.info(`Migration ${migration.version} rolled back`); - } - - logger.info('Rollback completed successfully'); - } catch (error) { - logger.error('Rollback failed', { error: error.message }); - throw error; + // Remove migration record + await client.query( + 'DELETE FROM migrations WHERE version = $1', + [migration.version] + ) + }) + + logger.info(`Migration ${migration.version} rolled back`) + } + + logger.info('Rollback completed successfully') + } catch (error) { + logger.error('Rollback failed', { error: error.message }) + throw error + } } - } } // CLI interface async function main() { - const args = process.argv.slice(2); - const command = args[0]; + const args = process.argv.slice(2) + const command = args[0] - const runner = new MigrationRunner(); - - try { - switch (command) { - case 'up': - await runner.runMigrations(); - break; - case 'down': - const targetVersion = parseInt(args[1]) || 0; - await runner.rollback(targetVersion); - break; - case 'status': - const version = await runner.getCurrentVersion(); - logger.info(`Current database version: ${version}`); - break; - default: - logger.info('Usage: node migrate.js [up|down|status] [target_version]'); - logger.info(' up: Run pending migrations'); - logger.info(' down : Rollback to specified version'); - logger.info(' status: Show current version'); + const runner = new MigrationRunner() + + try { + switch (command) { + case 'up': { + await runner.runMigrations() + break + } + case 'down': { + const targetVersion = parseInt(args[1]) || 0 + await runner.rollback(targetVersion) + break + } + case 'status': { + const version = await runner.getCurrentVersion() + logger.info(`Current database version: ${version}`) + break + } + default: { + logger.info('Usage: node migrate.js [up|down|status] [target_version]') + logger.info(' up: Run pending migrations') + logger.info(' down : Rollback to specified version') + logger.info(' status: Show current version') + } + } + } catch (error) { + logger.error('Migration command failed', { error: error.message }) + process.exit(1) + } finally { + await database.close() } - } catch (error) { - logger.error('Migration command failed', { error: error.message }); - process.exit(1); - } finally { - await database.close(); - } } + // Run if called directly if (import.meta.url === `file://${process.argv[1]}`) { - main(); + main() } -export default MigrationRunner; +export default MigrationRunner diff --git a/src/server.js b/src/server.js index b735953..e0e5715 100644 --- a/src/server.js +++ b/src/server.js @@ -1,137 +1,142 @@ -import express from 'express'; -import helmet from 'helmet'; -import cors from 'cors'; -import compression from 'compression'; -import dotenv from 'dotenv'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -import logger from './config/logger.js'; -import database from './config/database.js'; -import uploadRoutes from './routes/upload.js'; -import chunkedUploadRoutes from './routes/chunkedUpload.js'; -import fileRoutes from './routes/files.js'; -import publicRoutes from './routes/public.js'; -import healthRoutes from './routes/health.js'; -import HomeController from './controllers/HomeController.js'; -import { errorHandler, notFoundHandler } from './middleware/errorHandler.js'; -import { requestLogger } from './middleware/requestLogger.js'; -import { globalLimiter, apiLimiter, fileLimiter } from './middleware/rateLimiter.js'; +import express from 'express' +import helmet from 'helmet' +import cors from 'cors' +import compression from 'compression' +import dotenv from 'dotenv' +import path from 'path' +import { fileURLToPath } from 'url' + +import logger from './config/logger.js' +import database from './config/database.js' +import uploadRoutes from './routes/upload.js' +import chunkedUploadRoutes from './routes/chunkedUpload.js' +import fileRoutes from './routes/files.js' +import publicRoutes from './routes/public.js' +import healthRoutes from './routes/health.js' +import HomeController from './controllers/HomeController.js' +import { errorHandler, notFoundHandler } from './middleware/errorHandler.js' +import { requestLogger } from './middleware/requestLogger.js' +import { globalLimiter, apiLimiter, fileLimiter } from './middleware/rateLimiter.js' // Load environment variables -dotenv.config(); +dotenv.config() -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +const __filename = fileURLToPath(import.meta.url) +// eslint-disable-next-line no-unused-vars +const __dirname = path.dirname(__filename) -const app = express(); -const PORT = process.env.PORT || 3000; +const app = express() +const PORT = process.env.PORT || 3000 // Set powered by header to otto app.use((req, res, next) => { - res.setHeader('X-Powered-By', 'otto'); - next(); -}); + res.setHeader('X-Powered-By', 'otto') + next() +}) // Trust proxy for Cloudflare -app.set('trust proxy', true); +app.set('trust proxy', true) // Security middleware app.use(helmet({ - contentSecurityPolicy: { - directives: { - defaultSrc: ["'self'", "*"], - styleSrc: ["'self'", "'unsafe-inline'", "*"], - scriptSrc: ["'self'", "*"], - imgSrc: ["'self'", "data:", "blob:", "*"], - mediaSrc: ["'self'", "*"], - connectSrc: ["'self'", "*"], + contentSecurityPolicy: { + directives: { + defaultSrc: ['\'self\'', '*'], + styleSrc: ['\'self\'', '\'unsafe-inline\'', '*'], + scriptSrc: ['\'self\'', '*'], + imgSrc: ['\'self\'', 'data:', 'blob:', '*'], + mediaSrc: ['\'self\'', '*'], + connectSrc: ['\'self\'', '*'], + }, }, - }, - crossOriginResourcePolicy: { policy: "cross-origin" }, - crossOriginOpenerPolicy: { policy: "same-origin-allow-popups" } -})); + crossOriginResourcePolicy: { policy: 'cross-origin' }, + crossOriginOpenerPolicy: { policy: 'same-origin-allow-popups' } +})) // CORS configuration - More permissive for CDN compatibility app.use(cors({ - origin: '*', - methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'Range'], - exposedHeaders: ['Content-Length', 'Content-Range', 'Accept-Ranges'], - maxAge: 86400, // 24 hours - credentials: false -})); + origin: '*', + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'Range'], + exposedHeaders: ['Content-Length', 'Content-Range', 'Accept-Ranges'], + maxAge: 86400, // 24 hours + credentials: false +})) // Apply global rate limiting -app.use(globalLimiter); -app.use(compression()); -app.use(express.json({ limit: '1mb' })); -app.use(express.urlencoded({ extended: true, limit: '1mb' })); +app.use(globalLimiter) +app.use(compression()) +app.use(express.json({ limit: '1mb' })) +app.use(express.urlencoded({ extended: true, limit: '1mb' })) -app.use(requestLogger); +app.use(requestLogger) // Homepage -app.get('/', HomeController.home); -app.get('/stats', HomeController.stats); +app.get('/', HomeController.home) +app.get('/stats', HomeController.stats) // Core routes with specific rate limiting -app.use('/upload', apiLimiter, uploadRoutes); -app.use('/upload/chunk', chunkedUploadRoutes); -app.use('/files', fileLimiter, fileRoutes); -app.use('/f', fileLimiter, fileRoutes); -app.use('/public', fileLimiter, publicRoutes); -app.use('/p', fileLimiter, publicRoutes); -app.use('/health', healthRoutes); +app.use('/upload', apiLimiter, uploadRoutes) +app.use('/upload/chunk', chunkedUploadRoutes) +app.use('/files', fileLimiter, fileRoutes) +app.use('/f', fileLimiter, fileRoutes) +app.use('/public', fileLimiter, publicRoutes) +app.use('/p', fileLimiter, publicRoutes) +app.use('/health', healthRoutes) // Error handling middleware -app.use(notFoundHandler); -app.use(errorHandler); +app.use(notFoundHandler) +app.use(errorHandler) // Initialize database and start server async function startServer() { - try { - // Test database connection (but don't fail if it's not available) try { - await database.testConnection(); - logger.info('Database connection established'); - } catch (dbError) { - logger.warn('Database connection failed - server will start but database features will be unavailable', { - error: dbError.message - }); } - - const server = app.listen(PORT, () => { - logger.info(`Otto file server running on port ${PORT}`); - logger.info(`Homepage: http://localhost:${PORT}`); - logger.info('Endpoints:'); - logger.info(' POST /upload - Upload files'); - logger.info(' GET /files/{id} - Get file by ID'); - logger.info(' GET /f/{id} - Short URL for files'); - logger.info(' GET /public/{context}/{filename} - Public files'); - logger.info(' GET /p/{context}/{filename} - Short URL for public files'); - logger.info(' GET /stats - Server statistics'); - logger.info(' GET /health - Health check'); - }); - - return server; - } catch (error) { - logger.error('Failed to start server:', error); - process.exit(1); - } + // Test database connection (but don't fail if it's not available) + try { + await database.testConnection() + logger.info('Database connection established') + } catch (dbError) { + logger.warn('Database connection failed - server will start but database features will be unavailable', { + error: dbError.message + }) } + + const server = app.listen(PORT, () => { + logger.info(`Otto file server running on port ${PORT}`) + logger.info(`Homepage: http://localhost:${PORT}`) + logger.info('Endpoints:') + logger.info(' POST /upload - Upload files') + logger.info(' GET /files/{id} - Get file by ID') + logger.info(' GET /f/{id} - Short URL for files') + logger.info(' GET /public/{context}/{filename} - Public files') + logger.info(' GET /p/{context}/{filename} - Short URL for public files') + logger.info(' GET /stats - Server statistics') + logger.info(' GET /health - Health check') + }) + + return server + } catch (error) { + logger.error('Failed to start server:', error) + process.exit(1) + } } // Graceful shutdown process.on('SIGTERM', async () => { - logger.info('SIGTERM received, shutting down gracefully'); - await database.close(); - process.exit(0); -}); + logger.info('SIGTERM received, shutting down gracefully') + await database.close() + process.exit(0) +}) process.on('SIGINT', async () => { - logger.info('SIGINT received, shutting down gracefully'); - await database.close(); - process.exit(0); -}); - -startServer(); + logger.info('SIGINT received, shutting down gracefully') + await database.close() + process.exit(0) +}) + +// Only start the HTTP server when not running tests. This allows importing +// the Express `app` in tests without starting a listening socket. +if (process.env.NODE_ENV !== 'test') { + startServer() +} -export default app; +export default app diff --git a/src/services/ChunkedUploadService.js b/src/services/ChunkedUploadService.js index 063737f..8570c4c 100644 --- a/src/services/ChunkedUploadService.js +++ b/src/services/ChunkedUploadService.js @@ -1,449 +1,449 @@ -import fs from 'fs'; -import path from 'path'; -import crypto from 'crypto'; -import { v4 as uuidv4 } from 'uuid'; -import logger from '../config/logger.js'; -import FileService from './FileService.js'; +import fs from 'fs' +import path from 'path' +import crypto from 'crypto' +import { v4 as uuidv4 } from 'uuid' +import logger from '../config/logger.js' +import FileService from './FileService.js' class ChunkedUploadService { - constructor() { + constructor() { // Configuration from environment or defaults - this.chunkSize = parseInt(process.env.CHUNK_SIZE) || 25 * 1024 * 1024; // 25MB default - this.sessionTimeout = parseInt(process.env.CHUNK_SESSION_TIMEOUT) || 24 * 60 * 60 * 1000; // 24 hours - this.maxConcurrentChunks = parseInt(process.env.MAX_CONCURRENT_CHUNKS) || 10; - this.tempDir = process.env.CHUNK_TEMP_DIR || path.join(process.cwd(), 'temp-chunks'); + this.chunkSize = parseInt(process.env.CHUNK_SIZE) || 25 * 1024 * 1024 // 25MB default + this.sessionTimeout = parseInt(process.env.CHUNK_SESSION_TIMEOUT) || 24 * 60 * 60 * 1000 // 24 hours + this.maxConcurrentChunks = parseInt(process.env.MAX_CONCURRENT_CHUNKS) || 10 + this.tempDir = process.env.CHUNK_TEMP_DIR || path.join(process.cwd(), 'temp-chunks') - // Ensure temp directory exists - this.ensureTempDir(); + // Ensure temp directory exists + this.ensureTempDir() - // In-memory session tracking (in production, use Redis or database) - this.uploadSessions = new Map(); + // In-memory session tracking (in production, use Redis or database) + this.uploadSessions = new Map() - // Cleanup expired sessions periodically - this.startCleanupTimer(); - } - - ensureTempDir() { - if (!fs.existsSync(this.tempDir)) { - fs.mkdirSync(this.tempDir, { recursive: true }); - logger.info('Created temp directory for chunked uploads', { path: this.tempDir }); + // Cleanup expired sessions periodically + this.startCleanupTimer() } - } - /** + ensureTempDir() { + if (!fs.existsSync(this.tempDir)) { + fs.mkdirSync(this.tempDir, { recursive: true }) + logger.info('Created temp directory for chunked uploads', { path: this.tempDir }) + } + } + + /** * Initialize a new chunked upload session */ - async initializeSession(options) { - const { - originalFilename, - totalSize, - totalChunks, - mimeType, - context = 'general', - uploadedBy = 'system', - uploadSource = 'api', - metadata = {} - } = options; - - const sessionId = uuidv4(); - const fileHash = crypto.createHash('sha256') - .update(`${originalFilename}-${totalSize}-${Date.now()}`) - .digest('hex'); - - const session = { - id: sessionId, - originalFilename, - totalSize, - totalChunks, - mimeType, - context, - uploadedBy, - uploadSource, - metadata, - fileHash, - createdAt: new Date(), - expiresAt: new Date(Date.now() + this.sessionTimeout), - chunks: new Map(), // chunkIndex -> { uploaded: boolean, path: string, size: number } - completed: false, - finalFilePath: null - }; - - this.uploadSessions.set(sessionId, session); - - logger.info('Chunked upload session initialized', { - sessionId, - originalFilename, - totalSize, - totalChunks, - context, - uploadedBy - }); - - return { - sessionId, - chunkSize: this.chunkSize, - expiresAt: session.expiresAt - }; - } - - /** + async initializeSession(options) { + const { + originalFilename, + totalSize, + totalChunks, + mimeType, + context = 'general', + uploadedBy = 'system', + uploadSource = 'api', + metadata = {} + } = options + + const sessionId = uuidv4() + const fileHash = crypto.createHash('sha256') + .update(`${originalFilename}-${totalSize}-${Date.now()}`) + .digest('hex') + + const session = { + id: sessionId, + originalFilename, + totalSize, + totalChunks, + mimeType, + context, + uploadedBy, + uploadSource, + metadata, + fileHash, + createdAt: new Date(), + expiresAt: new Date(Date.now() + this.sessionTimeout), + chunks: new Map(), // chunkIndex -> { uploaded: boolean, path: string, size: number } + completed: false, + finalFilePath: null + } + + this.uploadSessions.set(sessionId, session) + + logger.info('Chunked upload session initialized', { + sessionId, + originalFilename, + totalSize, + totalChunks, + context, + uploadedBy + }) + + return { + sessionId, + chunkSize: this.chunkSize, + expiresAt: session.expiresAt + } + } + + /** * Upload a single chunk */ - async uploadChunk(sessionId, chunkIndex, chunkBuffer, chunkSize) { - const session = this.uploadSessions.get(sessionId); - if (!session) { - throw new Error('Upload session not found or expired'); - } + async uploadChunk(sessionId, chunkIndex, chunkBuffer, chunkSize) { + const session = this.uploadSessions.get(sessionId) + if (!session) { + throw new Error('Upload session not found or expired') + } - if (this.isSessionExpired(session)) { - await this.cleanupSession(sessionId); - throw new Error('Upload session has expired'); - } + if (this.isSessionExpired(session)) { + await this.cleanupSession(sessionId) + throw new Error('Upload session has expired') + } - if (chunkIndex >= session.totalChunks || chunkIndex < 0) { - throw new Error('Invalid chunk index'); - } + if (chunkIndex >= session.totalChunks || chunkIndex < 0) { + throw new Error('Invalid chunk index') + } - // Create session-specific temp directory - const sessionTempDir = path.join(this.tempDir, sessionId); - if (!fs.existsSync(sessionTempDir)) { - fs.mkdirSync(sessionTempDir, { recursive: true }); - } + // Create session-specific temp directory + const sessionTempDir = path.join(this.tempDir, sessionId) + if (!fs.existsSync(sessionTempDir)) { + fs.mkdirSync(sessionTempDir, { recursive: true }) + } - const chunkPath = path.join(sessionTempDir, `chunk-${chunkIndex}`); + const chunkPath = path.join(sessionTempDir, `chunk-${chunkIndex}`) - // Write chunk to disk - fs.writeFileSync(chunkPath, chunkBuffer); - - // Update session - session.chunks.set(chunkIndex, { - uploaded: true, - path: chunkPath, - size: chunkSize, - uploadedAt: new Date() - }); - - const uploadedChunks = Array.from(session.chunks.values()).filter(c => c.uploaded).length; - const progress = (uploadedChunks / session.totalChunks) * 100; - - logger.info('Chunk uploaded', { - sessionId, - chunkIndex, - chunkSize, - progress: Math.round(progress), - uploadedChunks, - totalChunks: session.totalChunks - }); - - // Check if all chunks are uploaded - if (uploadedChunks === session.totalChunks) { - await this.assembleFile(sessionId); + // Write chunk to disk + fs.writeFileSync(chunkPath, chunkBuffer) + + // Update session + session.chunks.set(chunkIndex, { + uploaded: true, + path: chunkPath, + size: chunkSize, + uploadedAt: new Date() + }) + + const uploadedChunks = Array.from(session.chunks.values()).filter(c => c.uploaded).length + const progress = (uploadedChunks / session.totalChunks) * 100 + + logger.info('Chunk uploaded', { + sessionId, + chunkIndex, + chunkSize, + progress: Math.round(progress), + uploadedChunks, + totalChunks: session.totalChunks + }) + + // Check if all chunks are uploaded + if (uploadedChunks === session.totalChunks) { + await this.assembleFile(sessionId) + } + + return { + chunkIndex, + uploaded: true, + progress, + uploadedChunks, + totalChunks: session.totalChunks, + completed: session.completed + } } - return { - chunkIndex, - uploaded: true, - progress, - uploadedChunks, - totalChunks: session.totalChunks, - completed: session.completed - }; - } - - /** + /** * Get upload session status */ - getSessionStatus(sessionId) { - const session = this.uploadSessions.get(sessionId); - if (!session) { - return null; - } + getSessionStatus(sessionId) { + const session = this.uploadSessions.get(sessionId) + if (!session) { + return null + } - if (this.isSessionExpired(session)) { - this.cleanupSession(sessionId); - return null; - } + if (this.isSessionExpired(session)) { + this.cleanupSession(sessionId) + return null + } - const uploadedChunks = Array.from(session.chunks.values()).filter(c => c.uploaded); - const missingChunks = []; + const uploadedChunks = Array.from(session.chunks.values()).filter(c => c.uploaded) + const missingChunks = [] - for (let i = 0; i < session.totalChunks; i++) { - if (!session.chunks.has(i) || !session.chunks.get(i).uploaded) { - missingChunks.push(i); - } - } + for (let i = 0; i < session.totalChunks; i++) { + if (!session.chunks.has(i) || !session.chunks.get(i).uploaded) { + missingChunks.push(i) + } + } - return { - sessionId: session.id, - originalFilename: session.originalFilename, - totalSize: session.totalSize, - totalChunks: session.totalChunks, - uploadedChunks: uploadedChunks.length, - missingChunks, - progress: (uploadedChunks.length / session.totalChunks) * 100, - completed: session.completed, - createdAt: session.createdAt, - expiresAt: session.expiresAt - }; - } - /** + return { + sessionId: session.id, + originalFilename: session.originalFilename, + totalSize: session.totalSize, + totalChunks: session.totalChunks, + uploadedChunks: uploadedChunks.length, + missingChunks, + progress: (uploadedChunks.length / session.totalChunks) * 100, + completed: session.completed, + createdAt: session.createdAt, + expiresAt: session.expiresAt + } + } + /** * Assemble all chunks into final file */ - async assembleFile(sessionId) { - const session = this.uploadSessions.get(sessionId); - if (!session) { - throw new Error('Upload session not found'); - } + async assembleFile(sessionId) { + const session = this.uploadSessions.get(sessionId) + if (!session) { + throw new Error('Upload session not found') + } - if (session.completed) { - // Return the processed file object, not just the path - if (session.processedFile) { - return session.processedFile; - } else { - throw new Error('Session completed but no processed file found'); - } - } + if (session.completed) { + // Return the processed file object, not just the path + if (session.processedFile) { + return session.processedFile + } else { + throw new Error('Session completed but no processed file found') + } + } - // Verify all chunks are present - for (let i = 0; i < session.totalChunks; i++) { - if (!session.chunks.has(i) || !session.chunks.get(i).uploaded) { - throw new Error(`Missing chunk ${i}`); - } - } + // Verify all chunks are present + for (let i = 0; i < session.totalChunks; i++) { + if (!session.chunks.has(i) || !session.chunks.get(i).uploaded) { + throw new Error(`Missing chunk ${i}`) + } + } - // Create temporary file for assembly - const tempFileName = `${session.id}-${session.originalFilename}`; - const tempFilePath = path.join(this.tempDir, tempFileName); - const writeStream = fs.createWriteStream(tempFilePath); - - try { - // Assemble chunks in order - for (let i = 0; i < session.totalChunks; i++) { - const chunk = session.chunks.get(i); - const chunkData = fs.readFileSync(chunk.path); - writeStream.write(chunkData); - } - - writeStream.end(); - - // Wait for write to complete - await new Promise((resolve, reject) => { - writeStream.on('finish', resolve); - writeStream.on('error', reject); - }); - - // Verify file size - const stats = fs.statSync(tempFilePath); - if (stats.size !== session.totalSize) { - throw new Error(`Assembled file size (${stats.size}) doesn't match expected size (${session.totalSize})`); - } - - // Process the assembled file through the regular upload pipeline - const processedFile = await this.processAssembledFile(session, tempFilePath); - - if (!processedFile) { - throw new Error('No file was returned from processAssembledFile'); - } - - session.completed = true; - session.finalFilePath = processedFile.file_path; - session.processedFile = processedFile; - - logger.info('Chunked upload completed and assembled', { - sessionId, - originalFilename: session.originalFilename, - finalSize: stats.size, - fileId: processedFile.id - }); - - // Cleanup chunks (but keep session for a while for status queries) - await this.cleanupChunks(sessionId); - - return processedFile; - - } catch (error) { - // Cleanup on error - if (fs.existsSync(tempFilePath)) { - fs.unlinkSync(tempFilePath); - } - throw error; - } - } /** + // Create temporary file for assembly + const tempFileName = `${session.id}-${session.originalFilename}` + const tempFilePath = path.join(this.tempDir, tempFileName) + const writeStream = fs.createWriteStream(tempFilePath) + + try { + // Assemble chunks in order + for (let i = 0; i < session.totalChunks; i++) { + const chunk = session.chunks.get(i) + const chunkData = fs.readFileSync(chunk.path) + writeStream.write(chunkData) + } + + writeStream.end() + + // Wait for write to complete + await new Promise((resolve, reject) => { + writeStream.on('finish', resolve) + writeStream.on('error', reject) + }) + + // Verify file size + const stats = fs.statSync(tempFilePath) + if (stats.size !== session.totalSize) { + throw new Error(`Assembled file size (${stats.size}) doesn't match expected size (${session.totalSize})`) + } + + // Process the assembled file through the regular upload pipeline + const processedFile = await this.processAssembledFile(session, tempFilePath) + + if (!processedFile) { + throw new Error('No file was returned from processAssembledFile') + } + + session.completed = true + session.finalFilePath = processedFile.file_path + session.processedFile = processedFile + + logger.info('Chunked upload completed and assembled', { + sessionId, + originalFilename: session.originalFilename, + finalSize: stats.size, + fileId: processedFile.id + }) + + // Cleanup chunks (but keep session for a while for status queries) + await this.cleanupChunks(sessionId) + + return processedFile + + } catch (error) { + // Cleanup on error + if (fs.existsSync(tempFilePath)) { + fs.unlinkSync(tempFilePath) + } + throw error + } + } /** * Process assembled file through regular FileService */ - async processAssembledFile(session, tempFilePath) { - try { - // Get proper upload directory (same as regular upload middleware) - const uploadDir = process.env.UPLOAD_DIR || path.join(process.cwd(), 'uploads'); - const contextDir = path.join(uploadDir, session.context); + async processAssembledFile(session, tempFilePath) { + try { + // Get proper upload directory (same as regular upload middleware) + const uploadDir = process.env.UPLOAD_DIR || path.join(process.cwd(), 'uploads') + const contextDir = path.join(uploadDir, session.context) - // Ensure context directory exists - if (!fs.existsSync(contextDir)) { - fs.mkdirSync(contextDir, { recursive: true }); - } - - // Generate proper filename (similar to regular upload middleware) - const fileId = crypto.randomUUID(); - const extension = path.extname(session.originalFilename).toLowerCase(); - const finalFilename = `${fileId}${extension}`; - const finalFilePath = path.join(contextDir, finalFilename); - - // Move assembled file to proper location - fs.renameSync(tempFilePath, finalFilePath); - - logger.info('Moved assembled file to final location', { - sessionId: session.id, - from: tempFilePath, - to: finalFilePath - }); - - // Create a file object similar to multer format - const fileObj = { - path: finalFilePath, - originalname: session.originalFilename, - filename: finalFilename, - mimetype: session.mimeType, - size: session.totalSize, - encoding: '7bit', - fieldname: 'file' - }; - - // Process through FileService - const processedFiles = await FileService.processUploadedFiles([fileObj], { - context: session.context, - uploadedBy: session.uploadedBy, - uploadSource: session.uploadSource, - generateThumbnails: false, // Can be added as option later - metadata: { - ...session.metadata, - chunkedUpload: true, - sessionId: session.id, - originalTotalSize: session.totalSize, - totalChunks: session.totalChunks + // Ensure context directory exists + if (!fs.existsSync(contextDir)) { + fs.mkdirSync(contextDir, { recursive: true }) + } + + // Generate proper filename (similar to regular upload middleware) + const fileId = crypto.randomUUID() + const extension = path.extname(session.originalFilename).toLowerCase() + const finalFilename = `${fileId}${extension}` + const finalFilePath = path.join(contextDir, finalFilename) + + // Move assembled file to proper location + fs.renameSync(tempFilePath, finalFilePath) + + logger.info('Moved assembled file to final location', { + sessionId: session.id, + from: tempFilePath, + to: finalFilePath + }) + + // Create a file object similar to multer format + const fileObj = { + path: finalFilePath, + originalname: session.originalFilename, + filename: finalFilename, + mimetype: session.mimeType, + size: session.totalSize, + encoding: '7bit', + fieldname: 'file' + } + + // Process through FileService + const processedFiles = await FileService.processUploadedFiles([fileObj], { + context: session.context, + uploadedBy: session.uploadedBy, + uploadSource: session.uploadSource, + generateThumbnails: false, // Can be added as option later + metadata: { + ...session.metadata, + chunkedUpload: true, + sessionId: session.id, + originalTotalSize: session.totalSize, + totalChunks: session.totalChunks + } + }) + + if (!processedFiles || processedFiles.length === 0) { + throw new Error('FileService did not return any processed files') + } + + const processedFile = processedFiles[0] + if (!processedFile || !processedFile.id) { + throw new Error('FileService returned invalid file object') + } + + return processedFile + } catch (error) { + logger.error('Failed to process assembled file', { + sessionId: session.id, + error: error.message, + tempFilePath + }) + throw error } - }); - - if (!processedFiles || processedFiles.length === 0) { - throw new Error('FileService did not return any processed files'); - } - - const processedFile = processedFiles[0]; - if (!processedFile || !processedFile.id) { - throw new Error('FileService returned invalid file object'); - } - - return processedFile; - } catch (error) { - logger.error('Failed to process assembled file', { - sessionId: session.id, - error: error.message, - tempFilePath - }); - throw error; } - } - /** + /** * Cleanup chunks for a session */ - async cleanupChunks(sessionId) { - const session = this.uploadSessions.get(sessionId); - if (!session) return; - - const sessionTempDir = path.join(this.tempDir, sessionId); - if (fs.existsSync(sessionTempDir)) { - try { - // Remove all chunk files - const files = fs.readdirSync(sessionTempDir); - for (const file of files) { - const filePath = path.join(sessionTempDir, file); - fs.unlinkSync(filePath); - } - fs.rmdirSync(sessionTempDir); + async cleanupChunks(sessionId) { + const session = this.uploadSessions.get(sessionId) + if (!session) return + + const sessionTempDir = path.join(this.tempDir, sessionId) + if (fs.existsSync(sessionTempDir)) { + try { + // Remove all chunk files + const files = fs.readdirSync(sessionTempDir) + for (const file of files) { + const filePath = path.join(sessionTempDir, file) + fs.unlinkSync(filePath) + } + fs.rmdirSync(sessionTempDir) - logger.debug('Cleaned up chunks for session', { sessionId }); - } catch (error) { - logger.warn('Failed to cleanup chunks', { sessionId, error: error.message }); - } + logger.debug('Cleaned up chunks for session', { sessionId }) + } catch (error) { + logger.warn('Failed to cleanup chunks', { sessionId, error: error.message }) + } + } } - } - /** + /** * Cleanup entire session */ - async cleanupSession(sessionId) { - await this.cleanupChunks(sessionId); - this.uploadSessions.delete(sessionId); - logger.debug('Cleaned up session', { sessionId }); - } + async cleanupSession(sessionId) { + await this.cleanupChunks(sessionId) + this.uploadSessions.delete(sessionId) + logger.debug('Cleaned up session', { sessionId }) + } - /** + /** * Check if session is expired */ - isSessionExpired(session) { - return new Date() > session.expiresAt; - } + isSessionExpired(session) { + return new Date() > session.expiresAt + } - /** + /** * Start periodic cleanup of expired sessions */ - startCleanupTimer() { - setInterval(async () => { - const now = new Date(); - const expiredSessions = []; - - for (const [sessionId, session] of this.uploadSessions) { - if (now > session.expiresAt) { - expiredSessions.push(sessionId); - } - } - - for (const sessionId of expiredSessions) { - logger.info('Cleaning up expired session', { sessionId }); - await this.cleanupSession(sessionId); - } - - if (expiredSessions.length > 0) { - logger.info('Cleaned up expired sessions', { count: expiredSessions.length }); - } - }, 60 * 60 * 1000); // Run every hour - } + startCleanupTimer() { + setInterval(async () => { + const now = new Date() + const expiredSessions = [] + + for (const [sessionId, session] of this.uploadSessions) { + if (now > session.expiresAt) { + expiredSessions.push(sessionId) + } + } + + for (const sessionId of expiredSessions) { + logger.info('Cleaning up expired session', { sessionId }) + await this.cleanupSession(sessionId) + } + + if (expiredSessions.length > 0) { + logger.info('Cleaned up expired sessions', { count: expiredSessions.length }) + } + }, 60 * 60 * 1000) // Run every hour + } - /** + /** * Resume upload - get missing chunks for a session */ - getMissingChunks(sessionId) { - const status = this.getSessionStatus(sessionId); - if (!status) { - return null; + getMissingChunks(sessionId) { + const status = this.getSessionStatus(sessionId) + if (!status) { + return null + } + return status.missingChunks } - return status.missingChunks; - } - /** + /** * Cancel upload session */ - async cancelSession(sessionId) { - const session = this.uploadSessions.get(sessionId); - if (session) { - await this.cleanupSession(sessionId); - logger.info('Upload session cancelled', { sessionId }); - return true; + async cancelSession(sessionId) { + const session = this.uploadSessions.get(sessionId) + if (session) { + await this.cleanupSession(sessionId) + logger.info('Upload session cancelled', { sessionId }) + return true + } + return false } - return false; - } - /** + /** * Get current configuration */ - getConfig() { - return { - chunkSize: this.chunkSize, - sessionTimeout: this.sessionTimeout, - maxConcurrentChunks: this.maxConcurrentChunks, - tempDir: this.tempDir - }; - } + getConfig() { + return { + chunkSize: this.chunkSize, + sessionTimeout: this.sessionTimeout, + maxConcurrentChunks: this.maxConcurrentChunks, + tempDir: this.tempDir + } + } } -export default new ChunkedUploadService(); +export default new ChunkedUploadService() diff --git a/src/services/FileService.js b/src/services/FileService.js index d2bce70..53fc3f7 100644 --- a/src/services/FileService.js +++ b/src/services/FileService.js @@ -1,641 +1,641 @@ -import fs from 'fs'; -import path from 'path'; -import crypto from 'crypto'; -import sharp from 'sharp'; -import zlib from 'zlib'; -import { v4 as uuidv4 } from 'uuid'; -import FileModel from '../models/File.js'; -import logger from '../config/logger.js'; -import { isPublicContext } from '../config/publicContexts.js'; +import fs from 'fs' +import path from 'path' +import crypto from 'crypto' +import sharp from 'sharp' +import zlib from 'zlib' +import { v4 as uuidv4 } from 'uuid' +import FileModel from '../models/File.js' +import logger from '../config/logger.js' +import { isPublicContext } from '../config/publicContexts.js' class FileService { - async processUploadedFiles(files, options = {}) { - const { - context = 'general', - uploadedBy = 'system', - uploadSource = 'api', - generateThumbnails = false, - metadata = {}, - forcePublic = null - } = options; - - const processedFiles = []; - - for (const file of files) { - try { - // Calculate file hash for deduplication - const fileHash = await this.calculateFileHash(file.path); + async processUploadedFiles(files, options = {}) { + const { + context = 'general', + uploadedBy = 'system', + uploadSource = 'api', + generateThumbnails = false, + metadata = {}, + forcePublic = null + } = options + + const processedFiles = [] + + for (const file of files) { + try { + // Calculate file hash for deduplication + const fileHash = await this.calculateFileHash(file.path) - // Check if file already exists with same hash - const existingFile = await FileModel.findByHash(fileHash); - if (existingFile) { - // Check if the existing file actually exists on disk - if (fs.existsSync(existingFile.file_path)) { - logger.info('File already exists, reusing', { - hash: fileHash, - existingId: existingFile.id, - originalName: file.originalname, - existingPath: existingFile.file_path - }); + // Check if file already exists with same hash + const existingFile = await FileModel.findByHash(fileHash) + if (existingFile) { + // Check if the existing file actually exists on disk + if (fs.existsSync(existingFile.file_path)) { + logger.info('File already exists, reusing', { + hash: fileHash, + existingId: existingFile.id, + originalName: file.originalname, + existingPath: existingFile.file_path + }) - // Create new record pointing to same file but with current context - const fileId = uuidv4(); - const shouldBePublic = forcePublic !== null ? forcePublic : isPublicContext(context); + // Create new record pointing to same file but with current context + const fileId = uuidv4() + const shouldBePublic = forcePublic !== null ? forcePublic : isPublicContext(context) - const fileData = { - id: fileId, - filename: existingFile.filename, // Reuse existing filename - originalName: file.originalname, - filePath: existingFile.file_path, // Point to existing file - mimeType: file.mimetype, - fileSize: file.size, - uploadContext: context, - uploadedBy, - uploadSource, - isPublic: shouldBePublic, - fileHash, - metadata: { - ...metadata, - encoding: file.encoding, - fieldName: file.fieldname, - deduplicated: true, - originalFileId: existingFile.id - } - }; - - const savedFile = await FileModel.create(fileData); - processedFiles.push(savedFile); + const fileData = { + id: fileId, + filename: existingFile.filename, // Reuse existing filename + originalName: file.originalname, + filePath: existingFile.file_path, // Point to existing file + mimeType: file.mimetype, + fileSize: file.size, + uploadContext: context, + uploadedBy, + uploadSource, + isPublic: shouldBePublic, + fileHash, + metadata: { + ...metadata, + encoding: file.encoding, + fieldName: file.fieldname, + deduplicated: true, + originalFileId: existingFile.id + } + } + + const savedFile = await FileModel.create(fileData) + processedFiles.push(savedFile) - // Remove uploaded duplicate - fs.unlinkSync(file.path); + // Remove uploaded duplicate + fs.unlinkSync(file.path) - continue; - } else { - logger.warn('Existing file not found on disk, proceeding with new file', { - hash: fileHash, - existingId: existingFile.id, - missingPath: existingFile.file_path, - originalName: file.originalname - }); - // Don't reuse - let it process as a new file - } - }const fileId = uuidv4(); - const shouldBePublic = forcePublic !== null ? forcePublic : isPublicContext(context); + continue + } else { + logger.warn('Existing file not found on disk, proceeding with new file', { + hash: fileHash, + existingId: existingFile.id, + missingPath: existingFile.file_path, + originalName: file.originalname + }) + // Don't reuse - let it process as a new file + } + }const fileId = uuidv4() + const shouldBePublic = forcePublic !== null ? forcePublic : isPublicContext(context) - // Optimize images if enabled - let optimizationResult = { optimized: false, originalPath: file.path }; - if (file.mimetype.startsWith('image/')) { - optimizationResult = await this.optimizeImage(file.path, file.mimetype); - } + // Optimize images if enabled + let optimizationResult = { optimized: false, originalPath: file.path } + if (file.mimetype.startsWith('image/')) { + optimizationResult = await this.optimizeImage(file.path, file.mimetype) + } - // Compress file if beneficial - const compressionResult = await this.compressFile( - optimizationResult.optimizedPath || file.path, - file.mimetype - ); + // Compress file if beneficial + const compressionResult = await this.compressFile( + optimizationResult.optimizedPath || file.path, + file.mimetype + ) - const finalPath = compressionResult.compressedPath; - const finalSize = compressionResult.compressed ? - compressionResult.compressedSize : file.size; + const finalPath = compressionResult.compressedPath + const finalSize = compressionResult.compressed ? + compressionResult.compressedSize : file.size - const fileData = { - id: fileId, - filename: file.filename, - originalName: file.originalname, - filePath: finalPath, - mimeType: file.mimetype, - fileSize: finalSize, - uploadContext: context, - uploadedBy, - uploadSource, - isPublic: shouldBePublic, - fileHash, - metadata: { - ...metadata, - encoding: file.encoding, - fieldName: file.fieldname, - compressed: compressionResult.compressed, - compressionType: compressionResult.compressionType, - originalSize: compressionResult.originalSize, - optimized: optimizationResult.optimized - } - }; - - if (generateThumbnails && file.mimetype.startsWith('image/')) { - await this.generateThumbnail(finalPath, fileId); - fileData.metadata.hasThumbnail = true; - } - - const savedFile = await FileModel.create(fileData); - processedFiles.push(savedFile); - - logger.info('File processed successfully', { - fileId, - originalName: file.originalname, - size: file.size, - hash: fileHash - }); - - } catch (error) { - logger.error('Failed to process file', { - filename: file.originalname, - error: error.message - }); - - if (fs.existsSync(file.path)) { - fs.unlinkSync(file.path); + const fileData = { + id: fileId, + filename: file.filename, + originalName: file.originalname, + filePath: finalPath, + mimeType: file.mimetype, + fileSize: finalSize, + uploadContext: context, + uploadedBy, + uploadSource, + isPublic: shouldBePublic, + fileHash, + metadata: { + ...metadata, + encoding: file.encoding, + fieldName: file.fieldname, + compressed: compressionResult.compressed, + compressionType: compressionResult.compressionType, + originalSize: compressionResult.originalSize, + optimized: optimizationResult.optimized + } + } + + if (generateThumbnails && file.mimetype.startsWith('image/')) { + await this.generateThumbnail(finalPath, fileId) + fileData.metadata.hasThumbnail = true + } + + const savedFile = await FileModel.create(fileData) + processedFiles.push(savedFile) + + logger.info('File processed successfully', { + fileId, + originalName: file.originalname, + size: file.size, + hash: fileHash + }) + + } catch (error) { + logger.error('Failed to process file', { + filename: file.originalname, + error: error.message + }) + + if (fs.existsSync(file.path)) { + fs.unlinkSync(file.path) + } + + throw error + } } - throw error; - } + return processedFiles } - return processedFiles; - } - - async calculateFileHash(filePath) { - return new Promise((resolve, reject) => { - const hash = crypto.createHash('sha256'); - const stream = fs.createReadStream(filePath); + async calculateFileHash(filePath) { + return new Promise((resolve, reject) => { + const hash = crypto.createHash('sha256') + const stream = fs.createReadStream(filePath) - stream.on('data', (data) => hash.update(data)); - stream.on('end', () => resolve(hash.digest('hex'))); - stream.on('error', reject); - }); - } - async generateThumbnail(filePath, fileId) { - try { - const thumbnailDir = path.join(path.dirname(filePath), 'thumbnails'); - if (!fs.existsSync(thumbnailDir)) { - fs.mkdirSync(thumbnailDir, { recursive: true }); - } - - const thumbnailPath = path.join(thumbnailDir, `${fileId}_thumb.webp`); - - await sharp(filePath) - .resize(300, 300, { - fit: 'inside', - withoutEnlargement: true + stream.on('data', (data) => hash.update(data)) + stream.on('end', () => resolve(hash.digest('hex'))) + stream.on('error', reject) }) - .webp({ quality: 80 }) - .toFile(thumbnailPath); - - logger.debug('Thumbnail generated', { fileId, thumbnailPath }); - return thumbnailPath; - } catch (error) { - logger.error('Failed to generate thumbnail', { - fileId, - error: error.message - }); - // Don't throw - thumbnail generation is optional } - } - async compressFile(filePath, mimeType) { - try { - const originalSize = fs.statSync(filePath).size; + async generateThumbnail(filePath, fileId) { + try { + const thumbnailDir = path.join(path.dirname(filePath), 'thumbnails') + if (!fs.existsSync(thumbnailDir)) { + fs.mkdirSync(thumbnailDir, { recursive: true }) + } + + const thumbnailPath = path.join(thumbnailDir, `${fileId}_thumb.webp`) + + await sharp(filePath) + .resize(300, 300, { + fit: 'inside', + withoutEnlargement: true + }) + .webp({ quality: 80 }) + .toFile(thumbnailPath) + + logger.debug('Thumbnail generated', { fileId, thumbnailPath }) + return thumbnailPath + } catch (error) { + logger.error('Failed to generate thumbnail', { + fileId, + error: error.message + }) + // Don't throw - thumbnail generation is optional + } + } + async compressFile(filePath, mimeType) { + try { + const originalSize = fs.statSync(filePath).size - // Only compress text-based files and larger files - if (!this.shouldCompress(mimeType, originalSize)) { - return { - compressed: false, - originalSize, - compressedSize: originalSize, - compressionType: null, - compressedPath: filePath - }; - } - - const compressedPath = `${filePath}.gz`; + // Only compress text-based files and larger files + if (!this.shouldCompress(mimeType, originalSize)) { + return { + compressed: false, + originalSize, + compressedSize: originalSize, + compressionType: null, + compressedPath: filePath + } + } + + const compressedPath = `${filePath}.gz` - return new Promise((resolve, reject) => { - const readStream = fs.createReadStream(filePath); - const writeStream = fs.createWriteStream(compressedPath); - const gzip = zlib.createGzip({ level: 6 }); - - readStream - .pipe(gzip) - .pipe(writeStream) - .on('finish', () => { - const compressedSize = fs.statSync(compressedPath).size; - const compressionRatio = compressedSize / originalSize; - - // If compression doesn't save significant space, use original - if (compressionRatio > 0.9) { - fs.unlinkSync(compressedPath); - resolve({ + return new Promise((resolve, reject) => { + const readStream = fs.createReadStream(filePath) + const writeStream = fs.createWriteStream(compressedPath) + const gzip = zlib.createGzip({ level: 6 }) + + readStream + .pipe(gzip) + .pipe(writeStream) + .on('finish', () => { + const compressedSize = fs.statSync(compressedPath).size + const compressionRatio = compressedSize / originalSize + + // If compression doesn't save significant space, use original + if (compressionRatio > 0.9) { + fs.unlinkSync(compressedPath) + resolve({ + compressed: false, + originalSize, + compressedSize: originalSize, + compressionType: null, + compressedPath: filePath + }) + } else { + // Remove original, use compressed + fs.unlinkSync(filePath) + resolve({ + compressed: true, + originalSize, + compressedSize, + compressionType: 'gzip', + compressedPath + }) + } + }) + .on('error', reject) + }) + } catch (error) { + logger.error('File compression failed', { filePath, error: error.message }) + return { compressed: false, - originalSize, - compressedSize: originalSize, + originalSize: fs.statSync(filePath).size, + compressedSize: fs.statSync(filePath).size, compressionType: null, compressedPath: filePath - }); - } else { - // Remove original, use compressed - fs.unlinkSync(filePath); - resolve({ - compressed: true, - originalSize, - compressedSize, - compressionType: 'gzip', - compressedPath - }); } - }) - .on('error', reject); - }); - } catch (error) { - logger.error('File compression failed', { filePath, error: error.message }); - return { - compressed: false, - originalSize: fs.statSync(filePath).size, - compressedSize: fs.statSync(filePath).size, - compressionType: null, - compressedPath: filePath - }; + } } - } - shouldCompress(mimeType, fileSize) { + shouldCompress(mimeType, fileSize) { // Don't compress small files (< 1KB) - if (fileSize < 1024) return false; + if (fileSize < 1024) return false - // Don't compress already compressed formats - const compressedTypes = [ - 'image/jpeg', 'image/png', 'image/gif', 'image/webp', - 'video/', 'audio/', 'application/zip', 'application/gzip', - 'application/x-rar', 'application/x-7z-compressed' - ]; + // Don't compress already compressed formats + const compressedTypes = [ + 'image/jpeg', 'image/png', 'image/gif', 'image/webp', + 'video/', 'audio/', 'application/zip', 'application/gzip', + 'application/x-rar', 'application/x-7z-compressed' + ] - if (compressedTypes.some(type => mimeType.startsWith(type))) { - return false; - } + if (compressedTypes.some(type => mimeType.startsWith(type))) { + return false + } - // Compress text-based files - const compressibleTypes = [ - 'text/', 'application/json', 'application/xml', - 'application/javascript', 'application/css', - 'application/svg+xml' - ]; + // Compress text-based files + const compressibleTypes = [ + 'text/', 'application/json', 'application/xml', + 'application/javascript', 'application/css', + 'application/svg+xml' + ] - return compressibleTypes.some(type => mimeType.startsWith(type)); - } - async optimizeImage(filePath, mimeType) { - try { - if (!mimeType.startsWith('image/') || mimeType === 'image/gif') { - return { optimized: false, originalPath: filePath }; - } - - const optimizedPath = `${filePath}.optimized`; - const originalSize = fs.statSync(filePath).size; - - let sharpInstance = sharp(filePath); + return compressibleTypes.some(type => mimeType.startsWith(type)) + } + async optimizeImage(filePath, mimeType) { + try { + if (!mimeType.startsWith('image/') || mimeType === 'image/gif') { + return { optimized: false, originalPath: filePath } + } + + const optimizedPath = `${filePath}.optimized` + const originalSize = fs.statSync(filePath).size + + let sharpInstance = sharp(filePath) - // Apply format-specific optimizations - if (mimeType === 'image/jpeg') { - sharpInstance = sharpInstance.jpeg({ quality: 85, progressive: true }); - } else if (mimeType === 'image/png') { - sharpInstance = sharpInstance.png({ quality: 85, progressive: true }); - } else if (mimeType === 'image/webp') { - sharpInstance = sharpInstance.webp({ quality: 85 }); - } - - await sharpInstance.toFile(optimizedPath); - - const optimizedSize = fs.statSync(optimizedPath).size; + // Apply format-specific optimizations + if (mimeType === 'image/jpeg') { + sharpInstance = sharpInstance.jpeg({ quality: 85, progressive: true }) + } else if (mimeType === 'image/png') { + sharpInstance = sharpInstance.png({ quality: 85, progressive: true }) + } else if (mimeType === 'image/webp') { + sharpInstance = sharpInstance.webp({ quality: 85 }) + } + + await sharpInstance.toFile(optimizedPath) + + const optimizedSize = fs.statSync(optimizedPath).size - // If optimization saves significant space, use it - if (optimizedSize < originalSize * 0.9) { - fs.unlinkSync(filePath); - fs.renameSync(optimizedPath, filePath); + // If optimization saves significant space, use it + if (optimizedSize < originalSize * 0.9) { + fs.unlinkSync(filePath) + fs.renameSync(optimizedPath, filePath) - logger.info('Image optimized', { - originalSize, - optimizedSize, - savings: `${Math.round((1 - optimizedSize/originalSize) * 100)}%` - }); + logger.info('Image optimized', { + originalSize, + optimizedSize, + savings: `${Math.round((1 - optimizedSize/originalSize) * 100)}%` + }) - return { - optimized: true, - originalSize, - optimizedSize, - optimizedPath: filePath - }; - } else { - fs.unlinkSync(optimizedPath); - return { optimized: false, originalPath: filePath }; - } - } catch (error) { - logger.error('Image optimization failed', { filePath, error: error.message }); - return { optimized: false, originalPath: filePath }; + return { + optimized: true, + originalSize, + optimizedSize, + optimizedPath: filePath + } + } else { + fs.unlinkSync(optimizedPath) + return { optimized: false, originalPath: filePath } + } + } catch (error) { + logger.error('Image optimization failed', { filePath, error: error.message }) + return { optimized: false, originalPath: filePath } + } } - } - /** + /** * Get file by ID with access tracking * @param {string} fileId - File ID * @param {Object} options - Options * @returns {Object|null} File record */ - async getFile(fileId, options = {}) { - const { trackAccess = true } = options; + async getFile(fileId, options = {}) { + const { trackAccess = true } = options - try { - const file = await FileModel.findById(fileId); + try { + const file = await FileModel.findById(fileId) - if (!file) { - return null; - } - - // Update access count if requested - if (trackAccess) { - await FileModel.updateAccessCount(fileId); - } - - return file; - } catch (error) { - logger.error('Failed to get file', { fileId, error: error.message }); - throw error; + if (!file) { + return null + } + + // Update access count if requested + if (trackAccess) { + await FileModel.updateAccessCount(fileId) + } + + return file + } catch (error) { + logger.error('Failed to get file', { fileId, error: error.message }) + throw error + } } - } - /** + /** * Get file stream for serving * @param {string} filePath - File path * @returns {ReadStream} File stream */ - getFileStream(filePath) { - if (!fs.existsSync(filePath)) { - throw new Error('File not found on disk'); - } + getFileStream(filePath) { + if (!fs.existsSync(filePath)) { + throw new Error('File not found on disk') + } - return fs.createReadStream(filePath); - } + return fs.createReadStream(filePath) + } - /** + /** * Delete file (soft delete in DB, actual file removal) * @param {string} fileId - File ID to delete * @returns {boolean} Success status */ - async deleteFile(fileId) { - try { - const file = await FileModel.findById(fileId); + async deleteFile(fileId) { + try { + const file = await FileModel.findById(fileId) - if (!file) { - return false; - } - - // Soft delete in database - await FileModel.softDelete(fileId); - - // Remove actual file - if (fs.existsSync(file.file_path)) { - fs.unlinkSync(file.file_path); - logger.info('File deleted from disk', { fileId, path: file.file_path }); - } - - // Remove thumbnail if exists - const thumbnailPath = this.getThumbnailPath(file.file_path, fileId); - if (fs.existsSync(thumbnailPath)) { - fs.unlinkSync(thumbnailPath); - logger.debug('Thumbnail deleted', { fileId }); - } - - logger.info('File deleted successfully', { fileId }); - return true; - } catch (error) { - logger.error('Failed to delete file', { fileId, error: error.message }); - throw error; + if (!file) { + return false + } + + // Soft delete in database + await FileModel.softDelete(fileId) + + // Remove actual file + if (fs.existsSync(file.file_path)) { + fs.unlinkSync(file.file_path) + logger.info('File deleted from disk', { fileId, path: file.file_path }) + } + + // Remove thumbnail if exists + const thumbnailPath = this.getThumbnailPath(file.file_path, fileId) + if (fs.existsSync(thumbnailPath)) { + fs.unlinkSync(thumbnailPath) + logger.debug('Thumbnail deleted', { fileId }) + } + + logger.info('File deleted successfully', { fileId }) + return true + } catch (error) { + logger.error('Failed to delete file', { fileId, error: error.message }) + throw error + } } - } - /** + /** * Get thumbnail path for a file * @param {string} originalPath - Original file path * @param {string} fileId - File ID * @returns {string} Thumbnail path */ - getThumbnailPath(originalPath, fileId) { - const thumbnailDir = path.join(path.dirname(originalPath), 'thumbnails'); - return path.join(thumbnailDir, `${fileId}_thumb.webp`); - } - /** + getThumbnailPath(originalPath, fileId) { + const thumbnailDir = path.join(path.dirname(originalPath), 'thumbnails') + return path.join(thumbnailDir, `${fileId}_thumb.webp`) + } + /** * Get files by context with pagination * @param {string} context - Upload context * @param {Object} options - Query options * @returns {Array} File records */ - async getFilesByContext(context, options = {}) { - const { limit = 50, offset = 0 } = options; - - try { - return await FileModel.findByContext(context, limit, offset); - } catch (error) { - logger.error('Failed to get files by context', { - context, - error: error.message - }); - throw error; + async getFilesByContext(context, options = {}) { + const { limit = 50, offset = 0 } = options + + try { + return await FileModel.findByContext(context, limit, offset) + } catch (error) { + logger.error('Failed to get files by context', { + context, + error: error.message + }) + throw error + } } - } - /** + /** * Get public file by context and filename * @param {string} context - Upload context * @param {string} filename - Original filename * @param {Object} options - Options * @returns {Object|null} File record */ - async getPublicFileByContextAndFilename(context, filename, options = {}) { - try { - const file = await FileModel.findPublicByContextAndFilename(context, filename); + async getPublicFileByContextAndFilename(context, filename, options = {}) { + try { + const file = await FileModel.findPublicByContextAndFilename(context, filename) - if (file && options.trackAccess !== false) { - // Update access count asynchronously - FileModel.updateAccessCount(file.id).catch(err => { - logger.warn('Failed to update access count', { - fileId: file.id, - error: err.message - }); - }); - } + if (file && options.trackAccess !== false) { + // Update access count asynchronously + FileModel.updateAccessCount(file.id).catch(err => { + logger.warn('Failed to update access count', { + fileId: file.id, + error: err.message + }) + }) + } - return file; - } catch (error) { - logger.error('Failed to get public file by context and filename', { - context, - filename, - error: error.message - }); - throw error; - } - } /** + return file + } catch (error) { + logger.error('Failed to get public file by context and filename', { + context, + filename, + error: error.message + }) + throw error + } + } /** * Get public file by hash and context * @param {string} hash - File hash (first 12 characters) * @param {string} context - Upload context * @param {Object} options - Options * @returns {Object|null} File record */ - async getPublicFileByHash(hash, context, options = {}) { - try { - const file = await FileModel.findPublicByHashAndContext(hash, context); + async getPublicFileByHash(hash, context, options = {}) { + try { + const file = await FileModel.findPublicByHashAndContext(hash, context) - if (file && options.trackAccess !== false) { - // Update access count asynchronously - FileModel.updateAccessCount(file.id).catch(err => { - logger.warn('Failed to update access count', { - fileId: file.id, - error: err.message - }); - }); - } + if (file && options.trackAccess !== false) { + // Update access count asynchronously + FileModel.updateAccessCount(file.id).catch(err => { + logger.warn('Failed to update access count', { + fileId: file.id, + error: err.message + }) + }) + } - return file; - } catch (error) { - logger.error('Failed to get public file by hash', { - hash, - context, - error: error.message - }); - throw error; + return file + } catch (error) { + logger.error('Failed to get public file by hash', { + hash, + context, + error: error.message + }) + throw error + } } - } - /** + /** * Get public file by ID * @param {string} fileId - File ID * @param {Object} options - Options * @returns {Object|null} File record */ - async getPublicFile(fileId, options = {}) { - try { - const file = await FileModel.findPublicById(fileId); + async getPublicFile(fileId, options = {}) { + try { + const file = await FileModel.findPublicById(fileId) - if (file && options.trackAccess !== false) { - // Update access count asynchronously - FileModel.updateAccessCount(fileId).catch(err => { - logger.warn('Failed to update access count', { - fileId, - error: err.message - }); - }); - } + if (file && options.trackAccess !== false) { + // Update access count asynchronously + FileModel.updateAccessCount(fileId).catch(err => { + logger.warn('Failed to update access count', { + fileId, + error: err.message + }) + }) + } - return file; - } catch (error) { - logger.error('Failed to get public file', { - fileId, - error: error.message - }); - throw error; + return file + } catch (error) { + logger.error('Failed to get public file', { + fileId, + error: error.message + }) + throw error + } } - } - /** + /** * Get file by context and filename (authenticated) * @param {string} context - Upload context * @param {string} filename - Original filename * @param {Object} options - Options * @returns {Object|null} File record */ - async getFileByContextAndFilename(context, filename, options = {}) { - try { - const file = await FileModel.findByContextAndFilename(context, filename); + async getFileByContextAndFilename(context, filename, options = {}) { + try { + const file = await FileModel.findByContextAndFilename(context, filename) - if (file && options.trackAccess !== false) { - // Update access count asynchronously - FileModel.updateAccessCount(file.id).catch(err => { - logger.warn('Failed to update access count', { - fileId: file.id, - error: err.message - }); - }); - } + if (file && options.trackAccess !== false) { + // Update access count asynchronously + FileModel.updateAccessCount(file.id).catch(err => { + logger.warn('Failed to update access count', { + fileId: file.id, + error: err.message + }) + }) + } - return file; - } catch (error) { - logger.error('Failed to get file by context and filename', { - context, - filename, - error: error.message - }); - throw error; - } - }/** + return file + } catch (error) { + logger.error('Failed to get file by context and filename', { + context, + filename, + error: error.message + }) + throw error + } + }/** * Get files by uploader with pagination * @param {string} uploadedBy - Uploader identifier * @param {Object} options - Query options * @returns {Array} File records */ - async getFilesByUploader(uploadedBy, options = {}) { - const { limit = 50, offset = 0 } = options; - - try { - return await FileModel.findByUploadedBy(uploadedBy, limit, offset); - } catch (error) { - logger.error('Failed to get files by uploader', { - uploadedBy, - error: error.message - }); - throw error; + async getFilesByUploader(uploadedBy, options = {}) { + const { limit = 50, offset = 0 } = options + + try { + return await FileModel.findByUploadedBy(uploadedBy, limit, offset) + } catch (error) { + logger.error('Failed to get files by uploader', { + uploadedBy, + error: error.message + }) + throw error + } } - } - /** + /** * Validate file access permissions * @param {Object} file - File record * @param {Object} req - Request object * @returns {boolean} Access allowed */ - validateFileAccess(file, req) { + validateFileAccess(file, req) { // Service requests have full access - if (req.serviceAuthenticated) { - return true; - } + if (req.serviceAuthenticated) { + return true + } - // Public files are accessible to everyone - if (file.is_public) { - return true; - } + // Public files are accessible to everyone + if (file.is_public) { + return true + } - // Users can access their own files - if (req.user && file.uploaded_by === req.user.id) { - return true; - } + // Users can access their own files + if (req.user && file.uploaded_by === req.user.id) { + return true + } - // Upload token holders can access files from their context - if (req.uploadToken && file.upload_context === req.uploadToken.context) { - return true; - } + // Upload token holders can access files from their context + if (req.uploadToken && file.upload_context === req.uploadToken.context) { + return true + } - return false; - } - /** + return false + } + /** * Get file statistics * @returns {Object} File statistics */ - async getStats() { - try { - return await FileModel.getStats(); - } catch (error) { - logger.error('Failed to get file stats', { error: error.message }); - throw error; + async getStats() { + try { + return await FileModel.getStats() + } catch (error) { + logger.error('Failed to get file stats', { error: error.message }) + throw error + } } - } - /** + /** * Clean up old deleted files * @param {number} daysOld - Days old threshold * @returns {Array} Cleaned file IDs */ - async cleanupOldFiles(daysOld = 90) { - try { - const deletedFiles = await FileModel.cleanupOldFiles(daysOld); - logger.info('Old files cleaned up', { count: deletedFiles.length }); - return deletedFiles; - } catch (error) { - logger.error('Failed to cleanup old files', { error: error.message }); - throw error; + async cleanupOldFiles(daysOld = 90) { + try { + const deletedFiles = await FileModel.cleanupOldFiles(daysOld) + logger.info('Old files cleaned up', { count: deletedFiles.length }) + return deletedFiles + } catch (error) { + logger.error('Failed to cleanup old files', { error: error.message }) + throw error + } } - } } -export default new FileService(); +export default new FileService() diff --git a/src/services/TokenService.js b/src/services/TokenService.js index b6121ff..b0f5481 100644 --- a/src/services/TokenService.js +++ b/src/services/TokenService.js @@ -1,9 +1,9 @@ -import jwt from 'jsonwebtoken'; -import { v4 as uuidv4 } from 'uuid'; -import logger from '../config/logger.js'; +import jwt from 'jsonwebtoken' +import { v4 as uuidv4 } from 'uuid' +import logger from '../config/logger.js' class TokenService { - /** + /** * Generate a short-lived upload token for frontend uploads * @param {Object} options - Token options * @param {string} options.context - Upload context (e.g., 'profile-pictures', 'logos') @@ -13,121 +13,121 @@ class TokenService { * @param {string[]} options.allowedTypes - Allowed MIME types for this token * @returns {string} JWT token */ - generateUploadToken(options = {}) { - const { - context = 'general', - uploadedBy = 'anonymous', - maxFiles = 5, - maxSize = parseInt(process.env.MAX_FILE_SIZE) || 10485760, - allowedTypes = null, - expiresIn = process.env.UPLOAD_TOKEN_EXPIRES_IN || '15m' - } = options; + generateUploadToken(options = {}) { + const { + context = 'general', + uploadedBy = 'anonymous', + maxFiles = 5, + maxSize = parseInt(process.env.MAX_FILE_SIZE) || 10485760, + allowedTypes = null, + expiresIn = process.env.UPLOAD_TOKEN_EXPIRES_IN || '15m' + } = options - const tokenId = uuidv4(); - const payload = { - jti: tokenId, // JWT ID - type: 'upload', - context, - uploadedBy, - maxFiles, - maxSize, - allowedTypes, - iat: Math.floor(Date.now() / 1000) - }; + const tokenId = uuidv4() + const payload = { + jti: tokenId, // JWT ID + type: 'upload', + context, + uploadedBy, + maxFiles, + maxSize, + allowedTypes, + iat: Math.floor(Date.now() / 1000) + } - const token = jwt.sign(payload, process.env.JWT_SECRET, { - expiresIn, - issuer: 'otto-server', - audience: 'otto-upload' - }); + const token = jwt.sign(payload, process.env.JWT_SECRET, { + expiresIn, + issuer: 'otto-server', + audience: 'otto-upload' + }) - logger.info('Upload token generated', { - tokenId, - context, - uploadedBy, - expiresIn - }); + logger.info('Upload token generated', { + tokenId, + context, + uploadedBy, + expiresIn + }) - return { - token, - tokenId, - expiresIn, - context, - maxFiles, - maxSize, - allowedTypes - }; - } + return { + token, + tokenId, + expiresIn, + context, + maxFiles, + maxSize, + allowedTypes + } + } - /** + /** * Generate a service access token for file access * @param {Object} options - Token options * @returns {string} JWT token */ - generateAccessToken(options = {}) { - const { - userId, - roles = ['user'], - permissions = [], - expiresIn = process.env.JWT_EXPIRES_IN || '1h' - } = options; + generateAccessToken(options = {}) { + const { + userId, + roles = ['user'], + permissions = [], + expiresIn = process.env.JWT_EXPIRES_IN || '1h' + } = options - const payload = { - sub: userId, - type: 'access', - roles, - permissions, - iat: Math.floor(Date.now() / 1000) - }; + const payload = { + sub: userId, + type: 'access', + roles, + permissions, + iat: Math.floor(Date.now() / 1000) + } - const token = jwt.sign(payload, process.env.JWT_SECRET, { - expiresIn, - issuer: 'otto-server', - audience: 'otto-api' - }); + const token = jwt.sign(payload, process.env.JWT_SECRET, { + expiresIn, + issuer: 'otto-server', + audience: 'otto-api' + }) - logger.info('Access token generated', { userId, roles, expiresIn }); + logger.info('Access token generated', { userId, roles, expiresIn }) - return { - token, - expiresIn, - userId, - roles, - permissions - }; - } + return { + token, + expiresIn, + userId, + roles, + permissions + } + } - /** + /** * Verify and decode a token * @param {string} token - JWT token to verify * @returns {Object} Decoded token payload */ - verifyToken(token) { - try { - const decoded = jwt.verify(token, process.env.JWT_SECRET); - return decoded; - } catch (error) { - logger.warn('Token verification failed', { error: error.message }); - throw error; + verifyToken(token) { + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET) + return decoded + } catch (error) { + logger.warn('Token verification failed', { error: error.message }) + throw error + } } - } - /** + /** * Generate a signed URL for temporary file access * @param {string} fileId - File ID * @param {number} expiresIn - Expiration time in seconds * @returns {string} Signed URL */ - generateSignedUrl(fileId, expiresIn = 3600) { - const payload = { - fileId, - type: 'file_access', - exp: Math.floor(Date.now() / 1000) + expiresIn - }; + generateSignedUrl(fileId, expiresIn = 3600) { + const payload = { + fileId, + type: 'file_access', + exp: Math.floor(Date.now() / 1000) + expiresIn + } - const token = jwt.sign(payload, process.env.JWT_SECRET); - return `/api/files/${fileId}?token=${token}`; - } + const token = jwt.sign(payload, process.env.JWT_SECRET) + return `/api/files/${fileId}?token=${token}` + } } -export default new TokenService(); +export default new TokenService() diff --git a/start.js b/start.js index f5619eb..5a75bfa 100644 --- a/start.js +++ b/start.js @@ -1,79 +1,81 @@ #!/usr/bin/env node -import dotenv from 'dotenv'; -import database from './src/config/database.js'; +import dotenv from 'dotenv' +import database from './src/config/database.js' // Load environment variables -dotenv.config(); +dotenv.config() -console.log('šŸš€ Otto Server - Pre-flight Check'); -console.log('====================================='); +console.log('šŸš€ Otto Server - Pre-flight Check') +console.log('=====================================') // Check required environment variables const requiredEnvVars = [ - 'DB_HOST', - 'DB_NAME', - 'DB_USER', - 'JWT_SECRET', - 'SERVICE_TOKEN' -]; + 'DB_HOST', + 'DB_NAME', + 'DB_USER', + 'JWT_SECRET', + 'SERVICE_TOKEN' +] -let missingVars = []; +let missingVars = [] for (const envVar of requiredEnvVars) { - if (!process.env[envVar]) { - missingVars.push(envVar); - } + if (!process.env[envVar]) { + missingVars.push(envVar) + } } if (missingVars.length > 0) { - console.error('āŒ Missing required environment variables:'); - missingVars.forEach(varName => console.error(` - ${varName}`)); - console.error('\nPlease check your .env file and ensure all required variables are set.'); - process.exit(1); + console.error('āŒ Missing required environment variables:') + missingVars.forEach(varName => console.error(` - ${varName}`)) + console.error('\nPlease check your .env file and ensure all required variables are set.') + process.exit(1) } // Test database connection -console.log('šŸ” Testing database connection...'); +console.log('šŸ” Testing database connection...') try { - const result = await database.query('SELECT NOW() as current_time'); - console.log('āœ… Database connection successful'); - console.log(` Connected to: ${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}`); + // eslint-disable-next-line no-unused-vars + const result = await database.query('SELECT NOW() as current_time') + console.log('āœ… Database connection successful') + console.log(` Connected to: ${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}`) } catch (error) { - console.error('āŒ Database connection failed:', error.message); - console.error(' The server will start but database features will not work.'); - console.error(' Please check your database configuration and ensure PostgreSQL is running.'); + console.error('āŒ Database connection failed:', error.message) + console.error(' The server will start but database features will not work.') + console.error(' Please check your database configuration and ensure PostgreSQL is running.') } -console.log('āœ… Environment variables OK'); +console.log('āœ… Environment variables OK') // Test imports try { - const logger = await import('./src/config/logger.js'); - console.log('āœ… Logger module OK'); + // eslint-disable-next-line no-unused-vars + const logger = await import('./src/config/logger.js') + console.log('āœ… Logger module OK') - const database = await import('./src/config/database.js'); - console.log('āœ… Database module OK'); + const database = await import('./src/config/database.js') + console.log('āœ… Database module OK') - // Test database connection - try { - await database.default.testConnection(); - console.log('āœ… Database connection OK'); - } catch (error) { - console.warn('āš ļø Database connection failed:', error.message); - console.warn(' The server will start but database operations will fail.'); - console.warn(' Please ensure PostgreSQL is running and credentials are correct.'); - } + // Test database connection + try { + await database.default.testConnection() + console.log('āœ… Database connection OK') + } catch (error) { + console.warn('āš ļø Database connection failed:', error.message) + console.warn(' The server will start but database operations will fail.') + console.warn(' Please ensure PostgreSQL is running and credentials are correct.') + } - console.log('āœ… All checks passed'); - console.log('\nšŸš€ Starting Otto server...\n'); + console.log('āœ… All checks passed') + console.log('\nšŸš€ Starting Otto server...\n') - // Import and start the server - await import('./src/server.js'); + // Import and start the server + await import('./src/server.js') } catch (error) { - console.error('āŒ Startup failed:', error.message); - if (process.env.NODE_ENV === 'development') { - console.error('\nFull error:', error); - } - process.exit(1); + console.error('āŒ Startup failed:', error.message) + if (process.env.NODE_ENV === 'development') { + console.error('\nFull error:', error) + } + process.exit(1) }