From 454f31a61db986c99855d7fc30e95c37a66f7073 Mon Sep 17 00:00:00 2001 From: Lewechi Date: Mon, 27 Jul 2026 11:48:38 +0100 Subject: [PATCH 1/7] feat: soroban indexer infra --- .env.example | 6 + jest.config.js | 20 + package.json | 2 + pnpm-lock.yaml | 7064 +++++++++++++++++++ prisma.config.js | 13 + prisma/schema.prisma | 14 + src/app.js | 14 + src/config/database.js | 1 + src/config/environment.js | 51 + src/config/environment.ts | 11 + src/config/prisma.js | 18 + src/controllers/api-key.controllers.js | 57 + src/controllers/api-key.controllers.ts | 2 +- src/controllers/auth.controllers.js | 85 + src/controllers/index.js | 1 + src/controllers/invoice.controllers.js | 127 + src/controllers/invoice.controllers.ts | 8 +- src/controllers/merchant.controllers.js | 112 + src/controllers/pay.controllers.js | 69 + src/controllers/pay.controllers.ts | 6 +- src/entities/index.js | 1 + src/indexer/handlers/index.js | 1 + src/indexer/handlers/index.ts | 2 + src/indexer/poller.js | 148 + src/indexer/poller.ts | 163 + src/indexer/registry.js | 15 + src/indexer/registry.ts | 22 + src/indexer/run.js | 13 + src/indexer/run.ts | 16 + src/indexer/sorobanClient.js | 4 + src/indexer/sorobanClient.ts | 5 + src/indexer/types.js | 1 + src/indexer/types.ts | 7 + src/middlewares/auth.middleware.js | 129 + src/routes/auth.routes.js | 9 + src/routes/index.js | 11 + src/routes/invoice.routes.js | 12 + src/routes/merchant.routes.js | 16 + src/routes/pay.routes.js | 7 + src/server.js | 15 + src/services/api-key.services.js | 89 + src/services/auth.services.js | 100 + src/services/email.service.js | 112 + src/services/index.js | 1 + src/services/invoice-pdf.services.js | 95 + src/services/invoice.services.js | 143 + src/services/merchant.services.js | 191 + src/services/otp.services.js | 85 + src/services/pay.services.js | 93 + src/services/storage/invoice-pdf.storage.js | 3 + src/utils/api-key.utils.js | 15 + src/utils/errors.js | 9 + src/utils/invoice.validation.js | 121 + src/utils/slug.js | 9 + src/utils/validation.js | 76 + tests/unit/indexer.test.ts | 171 + 56 files changed, 9583 insertions(+), 8 deletions(-) create mode 100644 jest.config.js create mode 100644 pnpm-lock.yaml create mode 100644 prisma.config.js create mode 100644 src/app.js create mode 100644 src/config/database.js create mode 100644 src/config/environment.js create mode 100644 src/config/prisma.js create mode 100644 src/controllers/api-key.controllers.js create mode 100644 src/controllers/auth.controllers.js create mode 100644 src/controllers/index.js create mode 100644 src/controllers/invoice.controllers.js create mode 100644 src/controllers/merchant.controllers.js create mode 100644 src/controllers/pay.controllers.js create mode 100644 src/entities/index.js create mode 100644 src/indexer/handlers/index.js create mode 100644 src/indexer/handlers/index.ts create mode 100644 src/indexer/poller.js create mode 100644 src/indexer/poller.ts create mode 100644 src/indexer/registry.js create mode 100644 src/indexer/registry.ts create mode 100644 src/indexer/run.js create mode 100644 src/indexer/run.ts create mode 100644 src/indexer/sorobanClient.js create mode 100644 src/indexer/sorobanClient.ts create mode 100644 src/indexer/types.js create mode 100644 src/indexer/types.ts create mode 100644 src/middlewares/auth.middleware.js create mode 100644 src/routes/auth.routes.js create mode 100644 src/routes/index.js create mode 100644 src/routes/invoice.routes.js create mode 100644 src/routes/merchant.routes.js create mode 100644 src/routes/pay.routes.js create mode 100644 src/server.js create mode 100644 src/services/api-key.services.js create mode 100644 src/services/auth.services.js create mode 100644 src/services/email.service.js create mode 100644 src/services/index.js create mode 100644 src/services/invoice-pdf.services.js create mode 100644 src/services/invoice.services.js create mode 100644 src/services/merchant.services.js create mode 100644 src/services/otp.services.js create mode 100644 src/services/pay.services.js create mode 100644 src/services/storage/invoice-pdf.storage.js create mode 100644 src/utils/api-key.utils.js create mode 100644 src/utils/errors.js create mode 100644 src/utils/invoice.validation.js create mode 100644 src/utils/slug.js create mode 100644 src/utils/validation.js create mode 100644 tests/unit/indexer.test.ts diff --git a/.env.example b/.env.example index 6a2abdf..1e5354c 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ # Server Configuration PORT=3000 NODE_ENV=development +DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres?schema=public" # JWT Secret (generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))") JWT_SECRET=your-jwt-secret-here @@ -18,3 +19,8 @@ SMTP_PORT=587 SMTP_USER= SMTP_PASS= SMTP_SECURE=false + +# Stellar Configuration +STELLAR_RPC_URL=https://soroban-testnet.stellar.org +STELLAR_CONTRACT_ID= +STELLAR_INDEXER_START_LEDGER= diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..5a1a4e3 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,20 @@ +const jestConfig = { + preset: 'ts-jest/presets/default-esm', + testEnvironment: 'node', + extensionsToTreatAsEsm: ['.ts'], + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + useESM: true, + }, + ], + }, + modulePathIgnorePatterns: ['/dist/'], + roots: ['/tests/'], + setupFiles: ['/tests/jest.setup.ts'], +}; +export default jestConfig; diff --git a/package.json b/package.json index 3bc5f73..a48c39a 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "build": "tsc", "start": "node src/server.js", "dev": "tsx watch src/server.ts", + "indexer": "tsx watch src/indexer/run.ts", "migration:generate": "typeorm-ts-node-commonjs migration:generate -d src/config/database.ts", "migration:run": "typeorm-ts-node-commonjs migration:run -d src/config/database.ts", "migration:revert": "typeorm-ts-node-commonjs migration:revert -d src/config/database.ts", @@ -55,6 +56,7 @@ "@types/node": "^22.19.11", "@types/nodemailer": "^8.0.1", "@types/supertest": "^6.0.3", + "@types/urijs": "^1.19.26", "@typescript-eslint/eslint-plugin": "^8.30.1", "@typescript-eslint/parser": "^8.30.1", "eslint": "^9.24.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..29385fc --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,7064 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@prisma/adapter-pg': + specifier: ^7.8.0 + version: 7.9.0 + '@prisma/client': + specifier: ^7.4.0 + version: 7.9.0(prisma@7.9.0(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3))(typescript@5.9.3) + '@stellar/stellar-sdk': + specifier: ^14.5.0 + version: 14.6.1 + '@types/pdfkit': + specifier: ^0.17.6 + version: 0.17.6 + bcrypt: + specifier: ^6.0.0 + version: 6.0.0 + cors: + specifier: ^2.8.5 + version: 2.8.6 + dotenv: + specifier: ^16.5.0 + version: 16.6.1 + express: + specifier: ^5.1.0 + version: 5.2.1 + helmet: + specifier: ^8.1.0 + version: 8.3.0 + jsonwebtoken: + specifier: ^9.0.3 + version: 9.0.3 + nodemailer: + specifier: ^9.0.1 + version: 9.0.3 + pdfkit: + specifier: ^0.19.1 + version: 0.19.1 + pg: + specifier: ^8.14.1 + version: 8.22.0 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + resend: + specifier: ^6.14.0 + version: 6.18.0 + typeorm: + specifier: ^0.3.22 + version: 0.3.31(mysql2@3.15.3)(pg@8.22.0)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.9.3)) + devDependencies: + '@eslint/eslintrc': + specifier: ^3.3.1 + version: 3.3.6 + '@eslint/js': + specifier: ^9.24.0 + version: 9.39.5 + '@types/bcrypt': + specifier: ^6.0.0 + version: 6.0.0 + '@types/cors': + specifier: ^2.8.19 + version: 2.8.19 + '@types/express': + specifier: ^5.0.1 + version: 5.0.6 + '@types/jest': + specifier: ^30.0.0 + version: 30.0.0 + '@types/jsonwebtoken': + specifier: ^9.0.10 + version: 9.0.10 + '@types/node': + specifier: ^22.19.11 + version: 22.20.1 + '@types/nodemailer': + specifier: ^8.0.1 + version: 8.0.1 + '@types/supertest': + specifier: ^6.0.3 + version: 6.0.3 + '@types/urijs': + specifier: ^1.19.26 + version: 1.19.26 + '@typescript-eslint/eslint-plugin': + specifier: ^8.30.1 + version: 8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.30.1 + version: 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint: + specifier: ^9.24.0 + version: 9.39.5(jiti@2.7.0) + eslint-config-prettier: + specifier: ^10.1.2 + version: 10.1.8(eslint@9.39.5(jiti@2.7.0)) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.6(eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0))(prettier@3.9.6) + jest: + specifier: ^30.2.0 + version: 30.4.2(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.9.3)) + jest-mock-extended: + specifier: ^4.0.0 + version: 4.0.1(@jest/globals@30.4.1)(jest@30.4.2(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.9.3)))(typescript@5.9.3) + nodemon: + specifier: ^3.1.9 + version: 3.1.14 + prettier: + specifier: ^3.5.3 + version: 3.9.6 + prisma: + specifier: ^7.4.0 + version: 7.9.0(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3) + supertest: + specifier: ^7.2.2 + version: 7.2.2 + ts-jest: + specifier: ^29.4.6 + version: 29.4.12(@babel/core@7.29.7)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@30.4.1(@babel/core@7.29.7))(jest-util@30.4.1)(jest@30.4.2(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.9.3)))(typescript@5.9.3) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@22.20.1)(typescript@5.9.3) + tsx: + specifier: ^4.21.0 + version: 4.23.1 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + typescript-eslint: + specifier: ^8.30.1 + version: 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + +packages: + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.29.7': + resolution: {integrity: sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.29.7': + resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.29.7': + resolution: {integrity: sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@electric-sql/pglite-socket@0.1.3': + resolution: {integrity: sha512-LAciWM0M1dCL8hlsxu2venbVZcdxema0BtDfpWYVqr+Y468UADw0pFWidhKw1M8sfJ8rdLT71tjMmnirf/IZRQ==} + hasBin: true + peerDependencies: + '@electric-sql/pglite': 0.4.3 + + '@electric-sql/pglite-tools@0.3.3': + resolution: {integrity: sha512-AlzLJTRJ8+UFgK8CmxIpyIpJ0+YaFw02IiOSdYrqxwPXdSyeIShz8aa9Tq+tYFXdPwcaMp/Fc80mQZ1dkOQ/wg==} + peerDependencies: + '@electric-sql/pglite': 0.4.3 + + '@electric-sql/pglite@0.4.3': + resolution: {integrity: sha512-ichuWTgtd4mOM1G4SpyGJa5trT03lWbMypDV0fUXUCXg5hiHqVAz/bZyV68NqmkLB7WcYmj1RMJVSp8HV/v/ZQ==} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.6': + resolution: {integrity: sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.5': + resolution: {integrity: sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.6': + resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} + engines: {node: '>=8'} + + '@jest/console@30.4.1': + resolution: {integrity: sha512-v3bhyxUh9Hgmo5p6hAOXe14/R3ZxZDOsvHleh4B07z3m/x4/ngPUXEm9XwK4sF4u+f+P2ORb0Ge+MgpaqRMVDA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/core@30.4.2': + resolution: {integrity: sha512-TZJA6cPJUFxoWhxaLo8t0VX/MZX2wPWr0uIDvLSHIvN4gu9h02vSzqI2kBADG1ExqQlC+cY09xKMSreivvrChQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/diff-sequences@30.4.0': + resolution: {integrity: sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/environment@30.4.1': + resolution: {integrity: sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/expect-utils@30.4.1': + resolution: {integrity: sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/expect@30.4.1': + resolution: {integrity: sha512-ginrj6TMgh2GshLUGCjO94Ptx9HhdZA/I6A9iUfyeLKFtdAjnKzHDgzgP9HYQgbxM1lbXScQ2eUBz2lGeVDPWA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/fake-timers@30.4.1': + resolution: {integrity: sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/get-type@30.1.0': + resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/globals@30.4.1': + resolution: {integrity: sha512-ZbuY4cmXC8DkxYjfvT2DbcHWL2T6vmsMhXCDcmTB2T0y0gaezBI77ufq5ZAIdcRkYZ7NEQEDg1xFeKbxUJ5v5Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/pattern@30.4.0': + resolution: {integrity: sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/reporters@30.4.1': + resolution: {integrity: sha512-/SnkPCzEQpUaBH81kjdEdDdo2WZl5hxw+BmLDGWjRkm8o7XlhjwsU36cqwe5PGBE5WYpBvDzRSdXx9rbGuJtNA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/schemas@30.4.1': + resolution: {integrity: sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/snapshot-utils@30.4.1': + resolution: {integrity: sha512-ObY4ljvQ95mt6iwKtVLetR/4yXiAgl3H4nJxhztr0MTjrN97TwDYrnCp/kF60Ec9HdhkWTHSu+Hg05aXfngpOA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/source-map@30.0.1': + resolution: {integrity: sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/test-result@30.4.1': + resolution: {integrity: sha512-/ZG7pgEiOmmWkN9TplKbOu4id2N5lh7FHwRwlkgBVAzGdRH+OkkQ8wX/kIxg4zmd3ZQvAL1RwL2yWsvNYYECTw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/test-sequencer@30.4.1': + resolution: {integrity: sha512-PeYE+4td5rKjoRPxztObrXU+H8hsjZfxKMXOcmrr34JerSyB/ROOxbbicz8B7A5j9R9VayDnVPvBmedqCsFCdw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/transform@30.4.1': + resolution: {integrity: sha512-Wz0LyktlTvRefoymh+n64hQ84KNXsRGcwdoZ8CSa0Ea+fgYcHZlnk+hDP7v2MS7il2bQ5uTEIxf4/NNfhMN4KQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/types@30.4.1': + resolution: {integrity: sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@noble/ciphers@1.3.0': + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.7': + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@paralleldrive/cuid2@2.3.1': + resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@pkgr/core@0.3.6': + resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} + engines: {node: ^14.18.0 || >=16.0.0} + + '@prisma/adapter-pg@7.9.0': + resolution: {integrity: sha512-kPYuFvNTlqnaFf2UpXBBG3ycTT3PL76uSZtLFBEwDytjMMUW8ZHrsb9cSNIarzdPW5EXWmBOeOq9/MVjMtbWkA==} + + '@prisma/client-runtime-utils@7.9.0': + resolution: {integrity: sha512-kMVmS4ZEy3xlkca+TfxOEm/ToVVlOS2x1Tc6/wIRf/HfczBqENtSPcKszy4ZpFNzjJ8SRKvlU5V0rrpoFw2KOg==} + + '@prisma/client@7.9.0': + resolution: {integrity: sha512-BTG/mB+WL/1sD2gWwdNc2uuVJjNNBgCDlPFdjco6jJArgbg4IAChtzVeW4debFa/NKBbsGedCjET316sjllWTQ==} + engines: {node: ^20.19 || ^22.12 || >=24.0} + peerDependencies: + prisma: '*' + typescript: '>=5.4.0' + peerDependenciesMeta: + prisma: + optional: true + typescript: + optional: true + + '@prisma/config@7.9.0': + resolution: {integrity: sha512-CsoK2mhl0u+N4/8V+XroQMOUNIic4isqD+E2HBG8l1yGEKo62CFDu3FHo0FdwItjl6XkW+omA1STSzeN1DAXlg==} + + '@prisma/debug@7.2.0': + resolution: {integrity: sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==} + + '@prisma/debug@7.9.0': + resolution: {integrity: sha512-i0KdVQuKUE6N9NloHs+sUNAk2c9svR3myBndQbA3BoeoArsSpwtNgTdHZL+wBtCLCcdS2OOC/PKhgTe36jkF5A==} + + '@prisma/dev@0.24.14': + resolution: {integrity: sha512-NhFO49O2JPTdzYiLHvceQn/HiwmcKF/iGV39ko3CpYsoGqS3rz3ko6gzuxFSIeHNwNJeuNcDexyyGeTO3DW80A==} + + '@prisma/driver-adapter-utils@7.9.0': + resolution: {integrity: sha512-fFXujitfMyjk3kOd1Tbs5FXBm6i2OWwEhaP5lHgkUM99jHpPEQwCWj+z/WKPFq6EDMThE1zGzSlVegtR0Pmu2w==} + + '@prisma/engines-version@7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad': + resolution: {integrity: sha512-2BsPPFksz3CQUXG6af3rVCtJKg6+JJGJTtfgu2fU8DdXhOfkBjulCq8mwybCd6ge0/jhZq2kOtLAbmUDMyI1nA==} + + '@prisma/engines@7.9.0': + resolution: {integrity: sha512-lDWJp/pgSWCLfYsupmmNo96jfsbQnH1yjia8XVM2Kh8nRZhD0bQU2jCHuy3ZTPMLR3apRD3k145ybENalAYjYw==} + + '@prisma/fetch-engine@7.9.0': + resolution: {integrity: sha512-F0XlIgjbE3EywRVR/HpCerNI/dxo40vK66tHcWpsWYwH/Jk9+FsICEzATeMsZ7bdnpZz93hkD4sAb5rKLsCCpA==} + + '@prisma/get-platform@7.2.0': + resolution: {integrity: sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==} + + '@prisma/get-platform@7.9.0': + resolution: {integrity: sha512-4awv6ATdgrHdLms0XKikCyfArn8BrUHZfqg0mtCKrI4+WJe24nmpsdwsypM9ozd03wa846AngY+zSbnngkMrXQ==} + + '@prisma/query-plan-executor@7.2.0': + resolution: {integrity: sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==} + + '@prisma/streams-local@0.1.11': + resolution: {integrity: sha512-0TcebL559MByKqTJ+SsrFIEg228iw8UCVRFckzgfRSiJqczhs+MuAgWOF9lnOIV/IVqvu+KMnFTH0eDeTQMpUg==} + engines: {bun: '>=1.2.0', node: '>=22.0.0'} + + '@prisma/studio-core@0.33.0': + resolution: {integrity: sha512-V2fX/nKEymNTrHXwfP26PGjoLStO35Ogu+ex7CFJbLrMYEcZxxZpiSNOs7px23Hk5mzLWvM5RsqG6Ka+rha+wg==} + engines: {node: ^20.19 || ^22.12 || >=24.0, pnpm: '8'} + peerDependencies: + '@types/react': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-toggle@1.1.10': + resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@sinclair/typebox@0.34.52': + resolution: {integrity: sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@15.4.0': + resolution: {integrity: sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==} + + '@sqltools/formatter@1.2.5': + resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} + + '@stablelib/base64@1.0.1': + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@stellar/js-xdr@3.1.2': + resolution: {integrity: sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ==} + + '@stellar/stellar-base@14.1.0': + resolution: {integrity: sha512-A8kFli6QGy22SRF45IjgPAJfUNGjnI+R7g4DF5NZYVsD1kGf7B4ITyc4OPclLV9tqNI4/lXxafGEw0JEUbHixw==} + engines: {node: '>=20.0.0'} + deprecated: This package is now rolled into @stellar/stellar-sdk. Please use @stellar/stellar-sdk to continue receiving updates and support. + + '@stellar/stellar-sdk@14.6.1': + resolution: {integrity: sha512-A1rQWDLdUasXkMXnYSuhgep+3ZZzyuXJKdt5/KAIc0gkmSp906HTvUpbT4pu+bVr41tu0+J4Ugz9J4BQAGGytg==} + engines: {node: '>=20.0.0'} + hasBin: true + + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + + '@tsconfig/node10@1.0.12': + resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/bcrypt@6.0.0': + resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cookiejar@2.1.5': + resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + + '@types/d3-array@3.0.3': + resolution: {integrity: sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==} + + '@types/d3-color@3.1.0': + resolution: {integrity: sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==} + + '@types/d3-delaunay@6.0.1': + resolution: {integrity: sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==} + + '@types/d3-format@3.0.1': + resolution: {integrity: sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-interpolate@3.0.1': + resolution: {integrity: sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.2': + resolution: {integrity: sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==} + + '@types/d3-shape@3.1.7': + resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} + + '@types/d3-time-format@2.1.0': + resolution: {integrity: sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA==} + + '@types/d3-time@3.0.0': + resolution: {integrity: sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/express-serve-static-core@5.1.2': + resolution: {integrity: sha512-d3KvEXBSo/lOAMc2u6fkyDHBvetBHeqD7wm/AcXfLpSOQwlmG9D/aQ0SFswVjv05p7ullQS7Mjohj6/VdbZuTg==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jest@30.0.0': + resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + + '@types/lodash@4.17.24': + resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} + + '@types/methods@1.1.4': + resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@22.20.1': + resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} + + '@types/nodemailer@8.0.1': + resolution: {integrity: sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw==} + + '@types/pdfkit@0.17.6': + resolution: {integrity: sha512-tIwzxk2uWKp0Cq9JIluQXJid77lYhF52EsIOwhsMF4iWLA6YneoBR1xVKYYdAysHuepUB0OX4tdwMiUDdGKmig==} + + '@types/pg@8.20.0': + resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + + '@types/qs@6.15.1': + resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + '@types/superagent@8.1.11': + resolution: {integrity: sha512-KA7srSW/HENDtOw9DOqaFLgWuMqN9WgjEw62lh9dpvRaZDkhdOkazASd7X7i2eMUYLHa1U37ZttnePsH5zTDHw==} + + '@types/supertest@6.0.3': + resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} + + '@types/urijs@1.19.26': + resolution: {integrity: sha512-wkXrVzX5yoqLnndOwFsieJA7oKM8cNkOKJtf/3vVGSUFkWDKZvFHpIl9Pvqb/T9UsawBBFMTTD8xu7sK5MWuvg==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.35': + resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + + '@typescript-eslint/eslint-plugin@8.65.0': + resolution: {integrity: sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.65.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.65.0': + resolution: {integrity: sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.65.0': + resolution: {integrity: sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.65.0': + resolution: {integrity: sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.65.0': + resolution: {integrity: sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.65.0': + resolution: {integrity: sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.65.0': + resolution: {integrity: sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.65.0': + resolution: {integrity: sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.65.0': + resolution: {integrity: sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.65.0': + resolution: {integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.3': + resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==} + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.12.2': + resolution: {integrity: sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + resolution: {integrity: sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.12.2': + resolution: {integrity: sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + resolution: {integrity: sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + resolution: {integrity: sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + resolution: {integrity: sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==} + cpu: [arm64] + os: [openharmony] + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + resolution: {integrity: sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + resolution: {integrity: sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + resolution: {integrity: sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + resolution: {integrity: sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==} + cpu: [x64] + os: [win32] + + '@visx/curve@4.0.1-alpha.0': + resolution: {integrity: sha512-jRu61Uz274pV1zyioXmboyrLutYbnKsgjj4njSGCnhdXj5GkZvZbg+ThDb6oOzoAnJOBRLz4rzPlWvNJOzuVMg==} + + '@visx/event@4.0.1-alpha.0': + resolution: {integrity: sha512-EQqCMSv/s8NbFjo+hz3FKsvvYfP+2QslsFJ/24/O5l/W+7UC6J6aAvO0ujVwrTwdYbuQ+vhxKi1xdPdKR/qj1g==} + + '@visx/grid@4.0.1-alpha.0': + resolution: {integrity: sha512-rycutGmTHO+znNdPumheWMglm7YfpffvRwUkVy5zy4WoORIuKTMkDxwnOzHG2xMxU3EE/YCd37xFV5AxA30yeg==} + peerDependencies: + react: ^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0 + + '@visx/group@4.0.1-alpha.0': + resolution: {integrity: sha512-V19l7iQ7jccBv8kao/EByuI6o4xtxzzLV9nqVI1hRvmdzTVsuLpqlwzYCZUXJaTVvUWf8s4D2SQFjGkj/Nw+0w==} + peerDependencies: + react: ^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0 + + '@visx/point@4.0.1-alpha.0': + resolution: {integrity: sha512-ijTfr/Nx09f03vIj9nyTr3z4Xth4Y75427UaogJh6dnIRLMEFHQOwNu791sbfiNj0a+ZXuaE32h0vKrFe4/8Qg==} + + '@visx/responsive@4.0.1-alpha.0': + resolution: {integrity: sha512-o+1zGywQZY0+yOx3Iw87wc4bbPJRr/HnIukTwfOz4UVyj9pB1OQNVHB7OORO1+LBHJceWpB31co/ZV9KHncKrA==} + peerDependencies: + react: ^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0 + + '@visx/scale@4.0.1-alpha.0': + resolution: {integrity: sha512-nzjeE87vFSAXGWFiiNfBpNLAf0Q8Qmf6syvKLjqNi4kGZkdhbUll3E/59YsgWXmjM8+llPLWzGsP+JPvo5eq1A==} + + '@visx/shape@4.0.1-alpha.0': + resolution: {integrity: sha512-62QeiVNmPlterQGwhkEDcbq7M0MqY0lBsK5QKXtM9ZoPZWkuGV3aykA3+Xu20B2FAvyJq4LqJzBc7Sxr+EAdbA==} + peerDependencies: + react: ^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0 + + '@visx/vendor@4.0.0-alpha.0': + resolution: {integrity: sha512-6I+MuqXBcv9jnlcVowHoHKSdk9gXTWkHLKyqBwRWg7LY6A3Ei8SHfubpqGV5rBUSppxMq2RszPJUS6w+H0YgmQ==} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@8.3.5: + resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} + engines: {node: '>=0.4.0'} + + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + ansis@4.3.1: + resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} + engines: {node: '>=14'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + app-root-path@3.1.0: + resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==} + engines: {node: '>= 6.0.0'} + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + + axios@1.18.1: + resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==} + + babel-jest@30.4.1: + resolution: {integrity: sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@babel/core': ^7.11.0 || ^8.0.0-0 + + babel-plugin-istanbul@7.0.1: + resolution: {integrity: sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==} + engines: {node: '>=12'} + + babel-plugin-jest-hoist@30.4.0: + resolution: {integrity: sha512-9EdtWM/sSfXLOGLwSn+GS6pIXyBnL07/8gyJlwFXjWy4DxMOyItqyUT29d4lQiS380EZwYlX7/At4PgBS+m2aA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + babel-preset-current-node-syntax@1.2.0: + resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} + peerDependencies: + '@babel/core': ^7.0.0 || ^8.0.0-0 + + babel-preset-jest@30.4.0: + resolution: {integrity: sha512-lBY4jxsNmCnSiu7kquw8ZC9F4+XLMOKypT3RnNHPvU2Kpd4W0xaPuLr5ZkRyOsvLYAY4yaW1ZwTW4xB7NIiZzg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@babel/core': ^7.11.0 || ^8.0.0-beta.1 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base32.js@0.1.0: + resolution: {integrity: sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==} + engines: {node: '>=0.12.0'} + + base64-js@0.0.8: + resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} + engines: {node: '>= 0.4'} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.11.4: + resolution: {integrity: sha512-s4+sLr9mZ/CyqeRritFeYV/Zx73OAtmaHn6kkBS1XRoJn1hrg3xIDUcpicAEX68tkcIN0iBCgti31C8zxtkhsQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + bcrypt@6.0.0: + resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==} + engines: {node: '>= 18'} + + better-result@2.10.0: + resolution: {integrity: sha512-oQhh0y1qo2/ZKdAAEvHZAqKKiHOFU5k/bW96fE2ScgQOVkJRiHwB+nOS1SgFsYqRlxMDWvefXi9Q3px7QvgNDw==} + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} + engines: {node: '>=18'} + + brace-expansion@1.1.16: + resolution: {integrity: sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==} + + brace-expansion@2.1.2: + resolution: {integrity: sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==} + + brace-expansion@5.0.8: + resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} + engines: {node: 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + brotli@1.3.3: + resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} + + browserify-zlib@0.2.0: + resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==} + + browserslist@4.28.7: + resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + c12@3.3.4: + resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==} + peerDependencies: + magicast: '*' + peerDependenciesMeta: + magicast: + optional: true + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001806: + resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + + cjs-module-lexer@2.2.0: + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} + + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + collect-v8-coverage@1.0.3: + resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + d3-array@3.2.1: + resolution: {integrity: sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==} + engines: {node: '>=12'} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.2: + resolution: {integrity: sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-geo@3.1.0: + resolution: {integrity: sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + dayjs@1.11.21: + resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + dedent@1.7.2: + resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge-ts@7.1.5: + resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} + engines: {node: '>=16.0.0'} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + + dfa@1.2.0: + resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} + + diff@4.0.4: + resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} + engines: {node: '>=0.3.1'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + effect@3.20.0: + resolution: {integrity: sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==} + + electron-to-chromium@1.5.396: + resolution: {integrity: sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==} + + elkjs@0.11.1: + resolution: {integrity: sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==} + + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + env-paths@3.0.0: + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.5.6: + resolution: {integrity: sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.5: + resolution: {integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource@2.0.2: + resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} + engines: {node: '>=12.0.0'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + exit-x@0.2.2: + resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} + engines: {node: '>= 0.8.0'} + + expect@30.4.1: + resolution: {integrity: sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + exsolve@1.1.0: + resolution: {integrity: sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==} + + fast-check@3.23.2: + resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} + engines: {node: '>=8.0.0'} + + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + + fast-uri@3.1.4: + resolution: {integrity: sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + feaxios@0.0.23: + resolution: {integrity: sha512-eghR0A21fvbkcQBgZuMfQhrXxJzC0GNUGC9fXhBge33D+mFDTwl0aJ35zoQQn575BhyjQitRc5N4f+L4cP708g==} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + find-my-way@9.6.0: + resolution: {integrity: sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==} + engines: {node: '>=20'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.3: + resolution: {integrity: sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==} + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + fontkit@2.0.4: + resolution: {integrity: sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + + formidable@3.5.4: + resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} + engines: {node: '>=14.0.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-port-please@3.2.0: + resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + giget@3.3.1: + resolution: {integrity: sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg==} + hasBin: true + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + grammex@3.1.13: + resolution: {integrity: sha512-LnPnhOBLEJEVKS8WFDVaA397L9Kq55Q9oSITJiVLHVdhAclfUkWzQv74KhvZHKL2Q09Pb1XdsrOsZ4LfTFFTEg==} + + graphmatch@1.1.1: + resolution: {integrity: sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==} + + handlebars@4.7.9: + resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + helmet@8.3.0: + resolution: {integrity: sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==} + engines: {node: '>=18.0.0'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.6: + resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {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. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + + is-retry-allowed@3.0.0: + resolution: {integrity: sha512-9xH0xvoggby+u0uGF7cZXdrutWiBiaFG8ZT4YFPXL8NzkyAwX3AKGLeFQLvzDpM430+nDFBZ1LHkie/8ocL06A==} + engines: {node: '>=12'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jest-changed-files@30.4.1: + resolution: {integrity: sha512-IuctmYrxi21iOSOaIXpJWalHyPAsVv0GeBHKDn8C1CA4W5htHn7INL+wdnL4Bo0+olEndvAFkmb++tIQJG+vvg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-circus@30.4.2: + resolution: {integrity: sha512-rvHH7VlY6LgbJXJTQ87GW62g1FntOtbhh0zT+v04kC+pgL6aBKyYINXxWukCpj3dcIBMw5/XUbtDS9dU9JTXeQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-cli@30.4.2: + resolution: {integrity: sha512-jfA2ocvVHMXS2QijrJ0d31ektP+d/W0T5RpcTX2Pq+3sVqHlsXVCM2+FmwpL+bdY8OfHpIg9xMxLF17Zg0U49Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@30.4.2: + resolution: {integrity: sha512-rNHAShJQqQwFNoL0hbf3BphSBOWnpOUAKvidLS/AjNVLPfoj5mSf4jQMfW3cYOs6hXeZC7nF7mDHaBnbxELOzg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@types/node': '*' + esbuild-register: '>=3.4.0' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + esbuild-register: + optional: true + ts-node: + optional: true + + jest-diff@30.4.1: + resolution: {integrity: sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-docblock@30.4.0: + resolution: {integrity: sha512-ZPMabUZCx5MpbZ2eBYSvZ0J8fvo3dR9oM+eeUpb3aKNQFuS2tu3Duw1TNlMoP8k3WQgKGJuhcMFvwcVuq6T7oA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-each@30.4.1: + resolution: {integrity: sha512-/8MJbH6fuj48TstjrMf+u/pd06Qezz5xOXvZA6442heNOWr8bdeoGZX2d9fCn028CoMgYmroH9//zky5GfyYmA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-environment-node@30.4.1: + resolution: {integrity: sha512-4FZYVOk85hz2AyT6BbarKy9u37g6DbrDyCdFhsnDdXqyrueYQvB+0zO4f/kqLCRD0BsPRXPMNJeQwihKZV8naw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-haste-map@30.4.1: + resolution: {integrity: sha512-rFrcONd8jeFsyw+Z9CrScJgglRf2+NFmNam8dKu7n+SoHqNYT47mn0DdEcVUZJpvh7Iz6/si7f7yUH7GJHVgnw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-leak-detector@30.4.1: + resolution: {integrity: sha512-IpmyiioeHxiWDhesHnUFmOxcTzwCwKpgACgWajtAP+nYQXiY7DakTxB6Bx9JFiRMljr0AX1PvnQdaU1KFoz6NQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-matcher-utils@30.4.1: + resolution: {integrity: sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-message-util@30.4.1: + resolution: {integrity: sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-mock-extended@4.0.1: + resolution: {integrity: sha512-Q/4k/yefiv/Al3n755V9xDEwMiL+7LwkjRKjaORkgCdovZv00hF/D0QypLoqO+MVfrYkzCYa4BYlcEKA74iOgQ==} + peerDependencies: + '@jest/globals': ^28.0.0 || ^29.0.0 || ^30.0.0 + jest: ^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0 || ^29.0.0 || ^30.0.0 + typescript: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 + + jest-mock@30.4.1: + resolution: {integrity: sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@30.4.0: + resolution: {integrity: sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-resolve-dependencies@30.4.2: + resolution: {integrity: sha512-gDiVh1I+GxYzz9oXlyw+1wv6VOYX1WYxMOfjsA3iGKePV2oxmbHhwxfkALxNxYy1ciw6APWwkW2zZONwP97aEQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-resolve@30.4.1: + resolution: {integrity: sha512-Zry8Yq/yJcNAZ7dJ5F2heic8AheXvbFZ7XI5V+h28nrYZ7Qoyy4dItq8OodjnYD270mvX+ZudmrNV9cysqhW5Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-runner@30.4.2: + resolution: {integrity: sha512-2dw0PslVYXxffXGpLo+Ejad+KcI1Qkjn7f4X4619gf21oCUmL+SPfjqIa/losUem3yEOvfNZe/F1HWUcNpODcg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-runtime@30.4.2: + resolution: {integrity: sha512-3/5e8iPz2k/VLqlr8DgTftYyLUv8Su3FkCAO2/Od81UsUTpSxOrS6O5x5KkoQwyUjmpYyDJKeyAvg2T2nvpNkQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-snapshot@30.4.1: + resolution: {integrity: sha512-tEOkkfOMppUyeiHwjZswOQ3lcnoTnws/q5FnGIaeIh/jmoU0ZlgMYRR8sTlTj+nNGCoJ0RDq6SfxGxCsyMTPmw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-util@30.4.1: + resolution: {integrity: sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-validate@30.4.1: + resolution: {integrity: sha512-PDWi4SOwLnwqNDfHZjOcsEFyZ4fc/2W2gVL3DEoyqnB6jCQMLRtfBong8s6omIw3lI0HWOus12xfnFmQtjW3fw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-watcher@30.4.1: + resolution: {integrity: sha512-/l9UonmvCwjHH7d2h3iAwIloLc1H0S8mJZ/LNK3i86hqwPAz8otUJjP9MfYtz9Tt77Su5FD2xGjZn8d31IZHlw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-worker@30.4.1: + resolution: {integrity: sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest@30.4.2: + resolution: {integrity: sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-md5@0.8.3: + resolution: {integrity: sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.15.0: + resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==} + hasBin: true + + js-yaml@4.3.0: + resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + linebreak@1.1.0: + resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru.min@1.1.4: + resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.1: + resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mysql2@3.15.3: + resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==} + engines: {node: '>= 8.0'} + + named-placeholders@1.1.6: + resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} + engines: {node: '>=8.0.0'} + + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + node-addon-api@8.9.0: + resolution: {integrity: sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q==} + engines: {node: ^18 || ^20 || >= 21} + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-releases@2.0.51: + resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} + engines: {node: '>=18'} + + nodemailer@9.0.3: + resolution: {integrity: sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==} + engines: {node: '>=6.0.0'} + + nodemon@3.1.14: + resolution: {integrity: sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==} + engines: {node: '>=10'} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pdfkit@0.19.1: + resolution: {integrity: sha512-6Gzk+wDwTs4VSxsR5rCMTnIl5nlmkye1oWB0l2hDB1EX6ZNSIBroKQEv+2+fPPn+stVjyqzmsqRJVDfB9fo5DA==} + + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + + pg-cloudflare@1.4.0: + resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} + + pg-connection-string@2.14.0: + resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.15.0: + resolution: {integrity: sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.22.0: + resolution: {integrity: sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + pkg-types@2.3.1: + resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + + png-js@1.1.0: + resolution: {integrity: sha512-PM/uYGzGdNSzqeOgly68+6wKQDL1SY0a/N+OEa/+br6LnHWOAJB0Npiamnodfq3jd2LS/i2fMeOKSAILjA+m5Q==} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postal-mime@2.7.5: + resolution: {integrity: sha512-GNEXKvWFQnbgO5NlrGzVa0FmWzBZ24PersAWErttSg1Hjpf0ATxTwS5DOMGaOpTG6bUh5cTr7xi0jAD942wCJA==} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-array@3.0.4: + resolution: {integrity: sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==} + engines: {node: '>=12'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + postgres@3.4.7: + resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} + engines: {node: '>=12'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} + engines: {node: '>=6.0.0'} + + prettier@3.9.6: + resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@30.4.1: + resolution: {integrity: sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + prisma@7.9.0: + resolution: {integrity: sha512-isQTJEK4pyOlAVzm6kBUDjzgdsgs0A/snpB38ycTHeOHW34qfepP+ClQltgDXqjZBnXALhEtE4duh9L3tN5fHw==} + engines: {node: ^20.19 || ^22.12 || >=24.0} + hasBin: true + peerDependencies: + better-sqlite3: '>=9.0.0' + typescript: '>=5.4.0' + peerDependenciesMeta: + better-sqlite3: + optional: true + typescript: + optional: true + + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + pure-rand@7.0.1: + resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} + + qs@6.15.3: + resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + engines: {node: '>=0.6'} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + range-parser@1.3.0: + resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + rc9@3.0.1: + resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==} + + react-dom@19.2.8: + resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} + peerDependencies: + react: ^19.2.8 + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-is@19.2.8: + resolution: {integrity: sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==} + + react@19.2.8: + resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} + engines: {node: '>=0.10.0'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + + remeda@2.33.4: + resolution: {integrity: sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resend@6.18.0: + resolution: {integrity: sha512-EjxZ9AVzywJgOlUoIJe9ytBWVrfbUtJbjeoLnRSvpU1sv97Hh9DSwhw+k8kiujrG4Rg4bzTBsjlmwWWuoOxSug==} + engines: {node: '>=20'} + peerDependencies: + '@react-email/render': '*' + peerDependenciesMeta: + '@react-email/render': + optional: true + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + restructure@3.0.2: + resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==} + + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex2@5.1.1: + resolution: {integrity: sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==} + hasBin: true + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + sha.js@2.4.12: + resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==} + engines: {node: '>= 0.10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + sql-highlight@6.1.0: + resolution: {integrity: sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==} + engines: {node: '>=14'} + + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + standardwebhooks@1.0.0: + resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + superagent@10.3.0: + resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==} + engines: {node: '>=14.18.0'} + + supertest@7.2.2: + resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==} + engines: {node: '>=14.18.0'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + synckit@0.11.13: + resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} + engines: {node: ^14.18.0 || >=16.0.0} + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + to-buffer@1.2.2: + resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} + engines: {node: '>= 0.4'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + toml@3.0.0: + resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + + touch@3.1.1: + resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} + hasBin: true + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-essentials@10.2.1: + resolution: {integrity: sha512-+Id1fRkuir+CsgK2x04/icS2b4V1hQmq7ObzIrDjhN0ozfRYivnP7aaKMVJfLApQm0trjR39A6NIMVchiB9Erw==} + peerDependencies: + typescript: '>=4.5.0' + peerDependenciesMeta: + typescript: + optional: true + + ts-jest@29.4.12: + resolution: {integrity: sha512-Ov6ClY53Fflh6BGAnY2DlTq1hYDrTycz2PVTXBWFW2CU+9zrEqAp9fWdGXl42EXO5RLSFAcAZ2JFKbP+zBTFfw==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 || ^30.0.0 + '@jest/types': ^29.0.0 || ^30.0.0 + babel-jest: ^29.0.0 || ^30.0.0 + esbuild: '*' + jest: ^29.0.0 || ^30.0.0 + jest-util: ^29.0.0 || ^30.0.0 + typescript: '>=4.3 <7' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + jest-util: + optional: true + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.23.1: + resolution: {integrity: sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typeorm@0.3.31: + resolution: {integrity: sha512-6u9EFtdLBgHjnPm78NStVeM+I/1MolTzKykDDcydzKUkh6E++YS6XViU/fePJbvDvEGU4Xq34KOM/CLeer9I2A==} + engines: {node: '>=16.13.0'} + hasBin: true + peerDependencies: + '@google-cloud/spanner': ^5.18.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + '@sap/hana-client': ^2.14.22 + better-sqlite3: ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 + ioredis: ^5.0.4 + mongodb: ^5.8.0 || ^6.0.0 + mssql: ^9.1.1 || ^10.0.0 || ^11.0.0 || ^12.0.0 + mysql2: ^2.2.5 || ^3.0.1 + oracledb: ^6.3.0 || ^7.0.0 + pg: ^8.5.1 + pg-native: ^3.0.0 + pg-query-stream: ^4.0.0 + redis: ^3.1.1 || ^4.0.0 || ^5.0.14 + sql.js: ^1.4.0 + sqlite3: ^5.0.3 || ^6.0.0 + ts-node: ^10.7.0 + typeorm-aurora-data-api-driver: ^2.0.0 || ^3.0.0 + peerDependenciesMeta: + '@google-cloud/spanner': + optional: true + '@sap/hana-client': + optional: true + better-sqlite3: + optional: true + ioredis: + optional: true + mongodb: + optional: true + mssql: + optional: true + mysql2: + optional: true + oracledb: + optional: true + pg: + optional: true + pg-native: + optional: true + pg-query-stream: + optional: true + redis: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + ts-node: + optional: true + typeorm-aurora-data-api-driver: + optional: true + + typescript-eslint@8.65.0: + resolution: {integrity: sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unicode-properties@1.4.1: + resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} + + unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unrs-resolver@1.12.2: + resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + urijs@1.19.11: + resolution: {integrity: sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==} + + uuid@11.1.1: + resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} + hasBin: true + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + + valibot@1.2.0: + resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + which-typed-array@1.1.22: + resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zeptomatch@2.1.0: + resolution: {integrity: sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==} + +snapshots: + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3(supports-color@5.5.0) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.7 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-import-attributes@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bcoe/v8-coverage@0.2.3': {} + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@electric-sql/pglite-socket@0.1.3(@electric-sql/pglite@0.4.3)': + dependencies: + '@electric-sql/pglite': 0.4.3 + + '@electric-sql/pglite-tools@0.3.3(@electric-sql/pglite@0.4.3)': + dependencies: + '@electric-sql/pglite': 0.4.3 + + '@electric-sql/pglite@0.4.3': {} + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(jiti@2.7.0))': + dependencies: + eslint: 9.39.5(jiti@2.7.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3(supports-color@5.5.0) + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.6': + dependencies: + ajv: 6.15.0 + debug: 4.4.3(supports-color@5.5.0) + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.3.0 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.5': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.15.0 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.6': {} + + '@jest/console@30.4.1': + dependencies: + '@jest/types': 30.4.1 + '@types/node': 22.20.1 + chalk: 4.1.2 + jest-message-util: 30.4.1 + jest-util: 30.4.1 + slash: 3.0.0 + + '@jest/core@30.4.2(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.9.3))': + dependencies: + '@jest/console': 30.4.1 + '@jest/pattern': 30.4.0 + '@jest/reporters': 30.4.1 + '@jest/test-result': 30.4.1 + '@jest/transform': 30.4.1 + '@jest/types': 30.4.1 + '@types/node': 22.20.1 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 4.4.0 + exit-x: 0.2.2 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-changed-files: 30.4.1 + jest-config: 30.4.2(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.9.3)) + jest-haste-map: 30.4.1 + jest-message-util: 30.4.1 + jest-regex-util: 30.4.0 + jest-resolve: 30.4.1 + jest-resolve-dependencies: 30.4.2 + jest-runner: 30.4.2 + jest-runtime: 30.4.2 + jest-snapshot: 30.4.1 + jest-util: 30.4.1 + jest-validate: 30.4.1 + jest-watcher: 30.4.1 + pretty-format: 30.4.1 + slash: 3.0.0 + transitivePeerDependencies: + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + '@jest/diff-sequences@30.4.0': {} + + '@jest/environment@30.4.1': + dependencies: + '@jest/fake-timers': 30.4.1 + '@jest/types': 30.4.1 + '@types/node': 22.20.1 + jest-mock: 30.4.1 + + '@jest/expect-utils@30.4.1': + dependencies: + '@jest/get-type': 30.1.0 + + '@jest/expect@30.4.1': + dependencies: + expect: 30.4.1 + jest-snapshot: 30.4.1 + transitivePeerDependencies: + - supports-color + + '@jest/fake-timers@30.4.1': + dependencies: + '@jest/types': 30.4.1 + '@sinonjs/fake-timers': 15.4.0 + '@types/node': 22.20.1 + jest-message-util: 30.4.1 + jest-mock: 30.4.1 + jest-util: 30.4.1 + + '@jest/get-type@30.1.0': {} + + '@jest/globals@30.4.1': + dependencies: + '@jest/environment': 30.4.1 + '@jest/expect': 30.4.1 + '@jest/types': 30.4.1 + jest-mock: 30.4.1 + transitivePeerDependencies: + - supports-color + + '@jest/pattern@30.4.0': + dependencies: + '@types/node': 22.20.1 + jest-regex-util: 30.4.0 + + '@jest/reporters@30.4.1': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 30.4.1 + '@jest/test-result': 30.4.1 + '@jest/transform': 30.4.1 + '@jest/types': 30.4.1 + '@jridgewell/trace-mapping': 0.3.31 + '@types/node': 22.20.1 + chalk: 4.1.2 + collect-v8-coverage: 1.0.3 + exit-x: 0.2.2 + glob: 10.5.0 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + jest-message-util: 30.4.1 + jest-util: 30.4.1 + jest-worker: 30.4.1 + slash: 3.0.0 + string-length: 4.0.2 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/schemas@30.4.1': + dependencies: + '@sinclair/typebox': 0.34.52 + + '@jest/snapshot-utils@30.4.1': + dependencies: + '@jest/types': 30.4.1 + chalk: 4.1.2 + graceful-fs: 4.2.11 + natural-compare: 1.4.0 + + '@jest/source-map@30.0.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + '@jest/test-result@30.4.1': + dependencies: + '@jest/console': 30.4.1 + '@jest/types': 30.4.1 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.3 + + '@jest/test-sequencer@30.4.1': + dependencies: + '@jest/test-result': 30.4.1 + graceful-fs: 4.2.11 + jest-haste-map: 30.4.1 + slash: 3.0.0 + + '@jest/transform@30.4.1': + dependencies: + '@babel/core': 7.29.7 + '@jest/types': 30.4.1 + '@jridgewell/trace-mapping': 0.3.31 + babel-plugin-istanbul: 7.0.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 30.4.1 + jest-regex-util: 30.4.0 + jest-util: 30.4.1 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + + '@jest/types@30.4.1': + dependencies: + '@jest/pattern': 30.4.0 + '@jest/schemas': 30.4.1 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 22.20.1 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@noble/ciphers@1.3.0': {} + + '@noble/curves@1.9.7': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.8.0': {} + + '@paralleldrive/cuid2@2.3.1': + dependencies: + '@noble/hashes': 1.8.0 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@pkgr/core@0.3.6': {} + + '@prisma/adapter-pg@7.9.0': + dependencies: + '@prisma/driver-adapter-utils': 7.9.0 + '@types/pg': 8.20.0 + pg: 8.22.0 + postgres-array: 3.0.4 + transitivePeerDependencies: + - pg-native + + '@prisma/client-runtime-utils@7.9.0': {} + + '@prisma/client@7.9.0(prisma@7.9.0(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3))(typescript@5.9.3)': + dependencies: + '@prisma/client-runtime-utils': 7.9.0 + optionalDependencies: + prisma: 7.9.0(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3) + typescript: 5.9.3 + + '@prisma/config@7.9.0': + dependencies: + c12: 3.3.4 + deepmerge-ts: 7.1.5 + effect: 3.20.0 + empathic: 2.0.0 + transitivePeerDependencies: + - magicast + + '@prisma/debug@7.2.0': {} + + '@prisma/debug@7.9.0': {} + + '@prisma/dev@0.24.14(typescript@5.9.3)': + dependencies: + '@electric-sql/pglite': 0.4.3 + '@electric-sql/pglite-socket': 0.1.3(@electric-sql/pglite@0.4.3) + '@electric-sql/pglite-tools': 0.3.3(@electric-sql/pglite@0.4.3) + '@prisma/get-platform': 7.2.0 + '@prisma/query-plan-executor': 7.2.0 + '@prisma/streams-local': 0.1.11 + find-my-way: 9.6.0 + foreground-child: 3.3.1 + get-port-please: 3.2.0 + pathe: 2.0.3 + proper-lockfile: 4.1.2 + remeda: 2.33.4 + std-env: 3.10.0 + valibot: 1.2.0(typescript@5.9.3) + zeptomatch: 2.1.0 + transitivePeerDependencies: + - typescript + + '@prisma/driver-adapter-utils@7.9.0': + dependencies: + '@prisma/debug': 7.9.0 + + '@prisma/engines-version@7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad': {} + + '@prisma/engines@7.9.0': + dependencies: + '@prisma/debug': 7.9.0 + '@prisma/engines-version': 7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad + '@prisma/fetch-engine': 7.9.0 + '@prisma/get-platform': 7.9.0 + + '@prisma/fetch-engine@7.9.0': + dependencies: + '@prisma/debug': 7.9.0 + '@prisma/engines-version': 7.9.0-1.e922089b7d7502aff4249d5da3420f6fa55fc6ad + '@prisma/get-platform': 7.9.0 + + '@prisma/get-platform@7.2.0': + dependencies: + '@prisma/debug': 7.2.0 + + '@prisma/get-platform@7.9.0': + dependencies: + '@prisma/debug': 7.9.0 + + '@prisma/query-plan-executor@7.2.0': {} + + '@prisma/streams-local@0.1.11': + dependencies: + ajv: 8.20.0 + better-result: 2.10.0 + env-paths: 3.0.0 + proper-lockfile: 4.1.2 + + '@prisma/studio-core@0.33.0(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/react-toggle': 1.1.10(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@types/react': 19.2.17 + '@visx/curve': 4.0.1-alpha.0 + '@visx/event': 4.0.1-alpha.0 + '@visx/grid': 4.0.1-alpha.0(react@19.2.8) + '@visx/group': 4.0.1-alpha.0(react@19.2.8) + '@visx/responsive': 4.0.1-alpha.0(react@19.2.8) + '@visx/scale': 4.0.1-alpha.0 + '@visx/shape': 4.0.1-alpha.0(react@19.2.8) + d3-array: 3.2.4 + d3-shape: 3.2.0 + elkjs: 0.11.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + transitivePeerDependencies: + - '@types/react-dom' + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.17)(react@19.2.8)': + dependencies: + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-primitive@2.1.3(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.17)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.17)(react@19.2.8)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.17)(react@19.2.8) + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-toggle@1.1.10(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-primitive': 2.1.3(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.17)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.17)(react@19.2.8)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.17)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.17)(react@19.2.8) + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.17)(react@19.2.8)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.17)(react@19.2.8) + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.17)(react@19.2.8)': + dependencies: + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.17 + + '@sinclair/typebox@0.34.52': {} + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@15.4.0': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@sqltools/formatter@1.2.5': {} + + '@stablelib/base64@1.0.1': {} + + '@standard-schema/spec@1.1.0': {} + + '@stellar/js-xdr@3.1.2': {} + + '@stellar/stellar-base@14.1.0': + dependencies: + '@noble/curves': 1.9.7 + '@stellar/js-xdr': 3.1.2 + base32.js: 0.1.0 + bignumber.js: 9.3.1 + buffer: 6.0.3 + sha.js: 2.4.12 + + '@stellar/stellar-sdk@14.6.1': + dependencies: + '@stellar/stellar-base': 14.1.0 + axios: 1.18.1 + bignumber.js: 9.3.1 + commander: 14.0.3 + eventsource: 2.0.2 + feaxios: 0.0.23 + randombytes: 2.1.0 + toml: 3.0.0 + urijs: 1.19.11 + transitivePeerDependencies: + - debug + - supports-color + + '@swc/helpers@0.5.23': + dependencies: + tslib: 2.8.1 + + '@tsconfig/node10@1.0.12': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/bcrypt@6.0.0': + dependencies: + '@types/node': 22.20.1 + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 22.20.1 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.20.1 + + '@types/cookiejar@2.1.5': {} + + '@types/cors@2.8.19': + dependencies: + '@types/node': 22.20.1 + + '@types/d3-array@3.0.3': {} + + '@types/d3-color@3.1.0': {} + + '@types/d3-delaunay@6.0.1': {} + + '@types/d3-format@3.0.1': {} + + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-interpolate@3.0.1': + dependencies: + '@types/d3-color': 3.1.0 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.2': + dependencies: + '@types/d3-time': 3.0.0 + + '@types/d3-shape@3.1.7': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@2.1.0': {} + + '@types/d3-time@3.0.0': {} + + '@types/estree@1.0.9': {} + + '@types/express-serve-static-core@5.1.2': + dependencies: + '@types/node': 22.20.1 + '@types/qs': 6.15.1 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.2 + '@types/serve-static': 2.2.0 + + '@types/geojson@7946.0.16': {} + + '@types/http-errors@2.0.5': {} + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jest@30.0.0': + dependencies: + expect: 30.4.1 + pretty-format: 30.4.1 + + '@types/json-schema@7.0.15': {} + + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 22.20.1 + + '@types/lodash@4.17.24': {} + + '@types/methods@1.1.4': {} + + '@types/ms@2.1.0': {} + + '@types/node@22.20.1': + dependencies: + undici-types: 6.21.0 + + '@types/nodemailer@8.0.1': + dependencies: + '@types/node': 22.20.1 + + '@types/pdfkit@0.17.6': + dependencies: + '@types/node': 22.20.1 + + '@types/pg@8.20.0': + dependencies: + '@types/node': 22.20.1 + pg-protocol: 1.15.0 + pg-types: 2.2.0 + + '@types/qs@6.15.1': {} + + '@types/range-parser@1.2.7': {} + + '@types/react@19.2.17': + dependencies: + csstype: 3.2.3 + + '@types/send@1.2.1': + dependencies: + '@types/node': 22.20.1 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 22.20.1 + + '@types/stack-utils@2.0.3': {} + + '@types/superagent@8.1.11': + dependencies: + '@types/cookiejar': 2.1.5 + '@types/methods': 1.1.4 + '@types/node': 22.20.1 + form-data: 4.0.6 + + '@types/supertest@6.0.3': + dependencies: + '@types/methods': 1.1.4 + '@types/superagent': 8.1.11 + + '@types/urijs@1.19.26': {} + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.35': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/type-utils': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.65.0 + eslint: 9.39.5(jiti@2.7.0) + ignore: 7.0.6 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.65.0 + debug: 4.4.3(supports-color@5.5.0) + eslint: 9.39.5(jiti@2.7.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.65.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@5.9.3) + '@typescript-eslint/types': 8.65.0 + debug: 4.4.3(supports-color@5.5.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.65.0': + dependencies: + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/visitor-keys': 8.65.0 + + '@typescript-eslint/tsconfig-utils@8.65.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + debug: 4.4.3(supports-color@5.5.0) + eslint: 9.39.5(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.65.0': {} + + '@typescript-eslint/typescript-estree@8.65.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.65.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@5.9.3) + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/visitor-keys': 8.65.0 + debug: 4.4.3(supports-color@5.5.0) + minimatch: 10.2.5 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) + eslint: 9.39.5(jiti@2.7.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.65.0': + dependencies: + '@typescript-eslint/types': 8.65.0 + eslint-visitor-keys: 5.0.1 + + '@ungap/structured-clone@1.3.3': {} + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + optional: true + + '@unrs/resolver-binding-android-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + optional: true + + '@visx/curve@4.0.1-alpha.0': + dependencies: + '@visx/vendor': 4.0.0-alpha.0 + + '@visx/event@4.0.1-alpha.0': + dependencies: + '@types/react': 19.2.17 + '@visx/point': 4.0.1-alpha.0 + + '@visx/grid@4.0.1-alpha.0(react@19.2.8)': + dependencies: + '@types/react': 19.2.17 + '@visx/curve': 4.0.1-alpha.0 + '@visx/group': 4.0.1-alpha.0(react@19.2.8) + '@visx/point': 4.0.1-alpha.0 + '@visx/scale': 4.0.1-alpha.0 + '@visx/shape': 4.0.1-alpha.0(react@19.2.8) + classnames: 2.5.1 + react: 19.2.8 + + '@visx/group@4.0.1-alpha.0(react@19.2.8)': + dependencies: + '@types/react': 19.2.17 + classnames: 2.5.1 + react: 19.2.8 + + '@visx/point@4.0.1-alpha.0': {} + + '@visx/responsive@4.0.1-alpha.0(react@19.2.8)': + dependencies: + '@types/lodash': 4.17.24 + '@types/react': 19.2.17 + lodash: 4.18.1 + react: 19.2.8 + + '@visx/scale@4.0.1-alpha.0': + dependencies: + '@visx/vendor': 4.0.0-alpha.0 + + '@visx/shape@4.0.1-alpha.0(react@19.2.8)': + dependencies: + '@types/lodash': 4.17.24 + '@types/react': 19.2.17 + '@visx/curve': 4.0.1-alpha.0 + '@visx/group': 4.0.1-alpha.0(react@19.2.8) + '@visx/scale': 4.0.1-alpha.0 + '@visx/vendor': 4.0.0-alpha.0 + classnames: 2.5.1 + lodash: 4.18.1 + react: 19.2.8 + + '@visx/vendor@4.0.0-alpha.0': + dependencies: + '@types/d3-array': 3.0.3 + '@types/d3-color': 3.1.0 + '@types/d3-delaunay': 6.0.1 + '@types/d3-format': 3.0.1 + '@types/d3-geo': 3.1.0 + '@types/d3-interpolate': 3.0.1 + '@types/d3-path': 3.1.1 + '@types/d3-scale': 4.0.2 + '@types/d3-shape': 3.1.7 + '@types/d3-time': 3.0.0 + '@types/d3-time-format': 2.1.0 + d3-array: 3.2.1 + d3-color: 3.1.0 + d3-delaunay: 6.0.2 + d3-format: 3.1.0 + d3-geo: 3.1.0 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + internmap: 2.0.3 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn-jsx@5.3.2(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + + acorn-walk@8.3.5: + dependencies: + acorn: 8.17.0 + + acorn@8.17.0: {} + + agent-base@6.0.2: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.4 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.3: {} + + ansis@4.3.1: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + app-root-path@3.1.0: {} + + arg@4.1.3: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + asap@2.0.6: {} + + asynckit@0.4.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + aws-ssl-profiles@1.1.2: {} + + axios@1.18.1: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.6 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + + babel-jest@30.4.1(@babel/core@7.29.7): + dependencies: + '@babel/core': 7.29.7 + '@jest/transform': 30.4.1 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 7.0.1 + babel-preset-jest: 30.4.0(@babel/core@7.29.7) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@7.0.1: + dependencies: + '@babel/helper-plugin-utils': 7.29.7 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.6 + istanbul-lib-instrument: 6.0.3 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@30.4.0: + dependencies: + '@types/babel__core': 7.20.5 + + babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.7): + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.7) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.7) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.7) + '@babel/plugin-syntax-import-attributes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.7) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.7) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.7) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.7) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.7) + + babel-preset-jest@30.4.0(@babel/core@7.29.7): + dependencies: + '@babel/core': 7.29.7 + babel-plugin-jest-hoist: 30.4.0 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.7) + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + base32.js@0.1.0: {} + + base64-js@0.0.8: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.11.4: {} + + bcrypt@6.0.0: + dependencies: + node-addon-api: 8.9.0 + node-gyp-build: 4.8.4 + + better-result@2.10.0: {} + + bignumber.js@9.3.1: {} + + binary-extensions@2.3.0: {} + + body-parser@2.3.0: + dependencies: + bytes: 3.1.2 + content-type: 2.0.0 + debug: 4.4.3(supports-color@5.5.0) + http-errors: 2.0.1 + iconv-lite: 0.7.3 + on-finished: 2.4.1 + qs: 6.15.3 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + + brace-expansion@1.1.16: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.1.2: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.8: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + brotli@1.3.3: + dependencies: + base64-js: 1.5.1 + + browserify-zlib@0.2.0: + dependencies: + pako: 1.0.11 + + browserslist@4.28.7: + dependencies: + baseline-browser-mapping: 2.11.4 + caniuse-lite: 1.0.30001806 + electron-to-chromium: 1.5.396 + node-releases: 2.0.51 + update-browserslist-db: 1.2.3(browserslist@4.28.7) + + bs-logger@0.2.6: + dependencies: + fast-json-stable-stringify: 2.1.0 + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + buffer-equal-constant-time@1.0.1: {} + + buffer-from@1.1.2: {} + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bytes@3.1.2: {} + + c12@3.3.4: + dependencies: + chokidar: 5.0.0 + confbox: 0.2.4 + defu: 6.1.7 + dotenv: 17.4.2 + exsolve: 1.1.0 + giget: 3.3.1 + jiti: 2.7.0 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + pkg-types: 2.3.1 + rc9: 3.0.1 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001806: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + char-regex@1.0.2: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + ci-info@4.4.0: {} + + cjs-module-lexer@2.2.0: {} + + classnames@2.5.1: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone@2.1.2: {} + + co@4.6.0: {} + + collect-v8-coverage@1.0.3: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@14.0.3: {} + + component-emitter@1.3.1: {} + + concat-map@0.0.1: {} + + confbox@0.2.4: {} + + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.0.0: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cookiejar@2.1.4: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + create-require@1.1.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + d3-array@3.2.1: + dependencies: + internmap: 2.0.3 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-delaunay@6.0.2: + dependencies: + delaunator: 5.1.0 + + d3-format@3.1.0: {} + + d3-geo@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + dayjs@1.11.21: {} + + debug@4.4.3(supports-color@5.5.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 5.5.0 + + dedent@1.7.2: {} + + deep-is@0.1.4: {} + + deepmerge-ts@7.1.5: {} + + deepmerge@4.3.1: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + defu@6.1.7: {} + + delaunator@5.1.0: + dependencies: + robust-predicates: 3.0.3 + + delayed-stream@1.0.0: {} + + denque@2.1.0: {} + + depd@2.0.0: {} + + destr@2.0.5: {} + + detect-newline@3.1.0: {} + + dezalgo@1.0.4: + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + + dfa@1.2.0: {} + + diff@4.0.4: {} + + dotenv@16.6.1: {} + + dotenv@17.4.2: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + effect@3.20.0: + dependencies: + '@standard-schema/spec': 1.1.0 + fast-check: 3.23.2 + + electron-to-chromium@1.5.396: {} + + elkjs@0.11.1: {} + + emittery@0.13.1: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + empathic@2.0.0: {} + + encodeurl@2.0.0: {} + + env-paths@3.0.0: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@2.0.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@2.7.0)): + dependencies: + eslint: 9.39.5(jiti@2.7.0) + + eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0))(prettier@3.9.6): + dependencies: + eslint: 9.39.5(jiti@2.7.0) + prettier: 3.9.6 + prettier-linter-helpers: 1.0.1 + synckit: 0.11.13 + optionalDependencies: + eslint-config-prettier: 10.1.8(eslint@9.39.5(jiti@2.7.0)) + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.5(jiti@2.7.0): + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.6 + '@eslint/js': 9.39.5 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@5.5.0) + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + eslint-visitor-keys: 4.2.1 + + esprima@4.0.1: {} + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + etag@1.8.1: {} + + eventsource@2.0.2: {} + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + exit-x@0.2.2: {} + + expect@30.4.1: + dependencies: + '@jest/expect-utils': 30.4.1 + '@jest/get-type': 30.1.0 + jest-matcher-utils: 30.4.1 + jest-message-util: 30.4.1 + jest-mock: 30.4.1 + jest-util: 30.4.1 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.3.0 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3(supports-color@5.5.0) + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.3 + range-parser: 1.3.0 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + exsolve@1.1.0: {} + + fast-check@3.23.2: + dependencies: + pure-rand: 6.1.0 + + fast-decode-uri-component@1.0.1: {} + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + + fast-safe-stringify@2.1.1: {} + + fast-sha256@1.3.0: {} + + fast-uri@3.1.4: {} + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + feaxios@0.0.23: + dependencies: + is-retry-allowed: 3.0.0 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-my-way@9.6.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 5.1.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.3 + keyv: 4.5.4 + + flatted@3.4.3: {} + + follow-redirects@1.16.0: {} + + fontkit@2.0.4: + dependencies: + '@swc/helpers': 0.5.23 + brotli: 1.3.3 + clone: 2.1.2 + dfa: 1.2.0 + fast-deep-equal: 3.1.3 + restructure: 3.0.2 + tiny-inflate: 1.0.3 + unicode-properties: 1.4.1 + unicode-trie: 2.0.0 + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + formidable@3.5.4: + dependencies: + '@paralleldrive/cuid2': 2.3.1 + dezalgo: 1.0.4 + once: 1.4.0 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-package-type@0.1.0: {} + + get-port-please@3.2.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-stream@6.0.1: {} + + giget@3.3.1: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@14.0.0: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + grammex@3.1.13: {} + + graphmatch@1.1.1: {} + + handlebars@4.7.9: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + helmet@8.3.0: {} + + html-escaper@2.0.2: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + human-signals@2.1.0: {} + + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore-by-default@1.0.1: {} + + ignore@5.3.2: {} + + ignore@7.0.6: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + internmap@2.0.3: {} + + ipaddr.js@1.9.1: {} + + is-arrayish@0.2.1: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-callable@1.2.7: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-generator-fn@2.1.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-promise@4.0.0: {} + + is-property@1.0.2: {} + + is-retry-allowed@3.0.0: {} + + is-stream@2.0.1: {} + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.22 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.7 + '@istanbuljs/schema': 0.1.6 + istanbul-lib-coverage: 3.2.2 + semver: 7.8.5 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3(supports-color@5.5.0) + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jest-changed-files@30.4.1: + dependencies: + execa: 5.1.1 + jest-util: 30.4.1 + p-limit: 3.1.0 + + jest-circus@30.4.2: + dependencies: + '@jest/environment': 30.4.1 + '@jest/expect': 30.4.1 + '@jest/test-result': 30.4.1 + '@jest/types': 30.4.1 + '@types/node': 22.20.1 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.7.2 + is-generator-fn: 2.1.0 + jest-each: 30.4.1 + jest-matcher-utils: 30.4.1 + jest-message-util: 30.4.1 + jest-runtime: 30.4.2 + jest-snapshot: 30.4.1 + jest-util: 30.4.1 + p-limit: 3.1.0 + pretty-format: 30.4.1 + pure-rand: 7.0.1 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@30.4.2(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.9.3)): + dependencies: + '@jest/core': 30.4.2(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.9.3)) + '@jest/test-result': 30.4.1 + '@jest/types': 30.4.1 + chalk: 4.1.2 + exit-x: 0.2.2 + import-local: 3.2.0 + jest-config: 30.4.2(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.9.3)) + jest-util: 30.4.1 + jest-validate: 30.4.1 + yargs: 17.7.3 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + jest-config@30.4.2(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.9.3)): + dependencies: + '@babel/core': 7.29.7 + '@jest/get-type': 30.1.0 + '@jest/pattern': 30.4.0 + '@jest/test-sequencer': 30.4.1 + '@jest/types': 30.4.1 + babel-jest: 30.4.1(@babel/core@7.29.7) + chalk: 4.1.2 + ci-info: 4.4.0 + deepmerge: 4.3.1 + glob: 10.5.0 + graceful-fs: 4.2.11 + jest-circus: 30.4.2 + jest-docblock: 30.4.0 + jest-environment-node: 30.4.1 + jest-regex-util: 30.4.0 + jest-resolve: 30.4.1 + jest-runner: 30.4.2 + jest-util: 30.4.1 + jest-validate: 30.4.1 + parse-json: 5.2.0 + pretty-format: 30.4.1 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.20.1 + ts-node: 10.9.2(@types/node@22.20.1)(typescript@5.9.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@30.4.1: + dependencies: + '@jest/diff-sequences': 30.4.0 + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + pretty-format: 30.4.1 + + jest-docblock@30.4.0: + dependencies: + detect-newline: 3.1.0 + + jest-each@30.4.1: + dependencies: + '@jest/get-type': 30.1.0 + '@jest/types': 30.4.1 + chalk: 4.1.2 + jest-util: 30.4.1 + pretty-format: 30.4.1 + + jest-environment-node@30.4.1: + dependencies: + '@jest/environment': 30.4.1 + '@jest/fake-timers': 30.4.1 + '@jest/types': 30.4.1 + '@types/node': 22.20.1 + jest-mock: 30.4.1 + jest-util: 30.4.1 + jest-validate: 30.4.1 + + jest-haste-map@30.4.1: + dependencies: + '@jest/types': 30.4.1 + '@types/node': 22.20.1 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 30.4.0 + jest-util: 30.4.1 + jest-worker: 30.4.1 + picomatch: 4.0.5 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-leak-detector@30.4.1: + dependencies: + '@jest/get-type': 30.1.0 + pretty-format: 30.4.1 + + jest-matcher-utils@30.4.1: + dependencies: + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + jest-diff: 30.4.1 + pretty-format: 30.4.1 + + jest-message-util@30.4.1: + dependencies: + '@babel/code-frame': 7.29.7 + '@jest/types': 30.4.1 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-util: 30.4.1 + picomatch: 4.0.5 + pretty-format: 30.4.1 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock-extended@4.0.1(@jest/globals@30.4.1)(jest@30.4.2(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.9.3)))(typescript@5.9.3): + dependencies: + '@jest/globals': 30.4.1 + jest: 30.4.2(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.9.3)) + lodash.isequal: 4.5.0 + ts-essentials: 10.2.1(typescript@5.9.3) + typescript: 5.9.3 + + jest-mock@30.4.1: + dependencies: + '@jest/types': 30.4.1 + '@types/node': 22.20.1 + jest-util: 30.4.1 + + jest-pnp-resolver@1.2.3(jest-resolve@30.4.1): + optionalDependencies: + jest-resolve: 30.4.1 + + jest-regex-util@30.4.0: {} + + jest-resolve-dependencies@30.4.2: + dependencies: + jest-regex-util: 30.4.0 + jest-snapshot: 30.4.1 + transitivePeerDependencies: + - supports-color + + jest-resolve@30.4.1: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 30.4.1 + jest-pnp-resolver: 1.2.3(jest-resolve@30.4.1) + jest-util: 30.4.1 + jest-validate: 30.4.1 + slash: 3.0.0 + unrs-resolver: 1.12.2 + + jest-runner@30.4.2: + dependencies: + '@jest/console': 30.4.1 + '@jest/environment': 30.4.1 + '@jest/test-result': 30.4.1 + '@jest/transform': 30.4.1 + '@jest/types': 30.4.1 + '@types/node': 22.20.1 + chalk: 4.1.2 + emittery: 0.13.1 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-docblock: 30.4.0 + jest-environment-node: 30.4.1 + jest-haste-map: 30.4.1 + jest-leak-detector: 30.4.1 + jest-message-util: 30.4.1 + jest-resolve: 30.4.1 + jest-runtime: 30.4.2 + jest-util: 30.4.1 + jest-watcher: 30.4.1 + jest-worker: 30.4.1 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + jest-runtime@30.4.2: + dependencies: + '@jest/environment': 30.4.1 + '@jest/fake-timers': 30.4.1 + '@jest/globals': 30.4.1 + '@jest/source-map': 30.0.1 + '@jest/test-result': 30.4.1 + '@jest/transform': 30.4.1 + '@jest/types': 30.4.1 + '@types/node': 22.20.1 + chalk: 4.1.2 + cjs-module-lexer: 2.2.0 + collect-v8-coverage: 1.0.3 + glob: 10.5.0 + graceful-fs: 4.2.11 + jest-haste-map: 30.4.1 + jest-message-util: 30.4.1 + jest-mock: 30.4.1 + jest-regex-util: 30.4.0 + jest-resolve: 30.4.1 + jest-snapshot: 30.4.1 + jest-util: 30.4.1 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + jest-snapshot@30.4.1: + dependencies: + '@babel/core': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/types': 7.29.7 + '@jest/expect-utils': 30.4.1 + '@jest/get-type': 30.1.0 + '@jest/snapshot-utils': 30.4.1 + '@jest/transform': 30.4.1 + '@jest/types': 30.4.1 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.7) + chalk: 4.1.2 + expect: 30.4.1 + graceful-fs: 4.2.11 + jest-diff: 30.4.1 + jest-matcher-utils: 30.4.1 + jest-message-util: 30.4.1 + jest-util: 30.4.1 + pretty-format: 30.4.1 + semver: 7.8.5 + synckit: 0.11.13 + transitivePeerDependencies: + - supports-color + + jest-util@30.4.1: + dependencies: + '@jest/types': 30.4.1 + '@types/node': 22.20.1 + chalk: 4.1.2 + ci-info: 4.4.0 + graceful-fs: 4.2.11 + picomatch: 4.0.5 + + jest-validate@30.4.1: + dependencies: + '@jest/get-type': 30.1.0 + '@jest/types': 30.4.1 + camelcase: 6.3.0 + chalk: 4.1.2 + leven: 3.1.0 + pretty-format: 30.4.1 + + jest-watcher@30.4.1: + dependencies: + '@jest/test-result': 30.4.1 + '@jest/types': 30.4.1 + '@types/node': 22.20.1 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 30.4.1 + string-length: 4.0.2 + + jest-worker@30.4.1: + dependencies: + '@types/node': 22.20.1 + '@ungap/structured-clone': 1.3.3 + jest-util: 30.4.1 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@30.4.2(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.9.3)): + dependencies: + '@jest/core': 30.4.2(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.9.3)) + '@jest/types': 30.4.1 + import-local: 3.2.0 + jest-cli: 30.4.2(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.9.3)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + jiti@2.7.0: {} + + js-md5@0.8.3: {} + + js-tokens@4.0.0: {} + + js-yaml@3.15.0: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.3.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.8.5 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + leven@3.1.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + linebreak@1.1.0: + dependencies: + base64-js: 0.0.8 + unicode-trie: 2.0.0 + + lines-and-columns@1.2.4: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isequal@4.5.0: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.memoize@4.1.2: {} + + lodash.merge@4.6.2: {} + + lodash.once@4.1.1: {} + + lodash@4.18.1: {} + + long@5.3.2: {} + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lru.min@1.1.4: {} + + make-dir@4.0.0: + dependencies: + semver: 7.8.5 + + make-error@1.3.6: {} + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + + math-intrinsics@1.1.0: {} + + media-typer@1.1.1: {} + + merge-descriptors@2.0.0: {} + + merge-stream@2.0.0: {} + + methods@1.1.2: {} + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mime@2.6.0: {} + + mimic-fn@2.1.0: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.8 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.16 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.2 + + minimist@1.2.8: {} + + minipass@7.1.3: {} + + ms@2.1.3: {} + + mysql2@3.15.3: + dependencies: + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.7.3 + long: 5.3.2 + lru.min: 1.1.4 + named-placeholders: 1.1.6 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + + named-placeholders@1.1.6: + dependencies: + lru.min: 1.1.4 + + napi-postinstall@0.3.4: {} + + natural-compare@1.4.0: {} + + negotiator@1.0.0: {} + + neo-async@2.6.2: {} + + node-addon-api@8.9.0: {} + + node-gyp-build@4.8.4: {} + + node-int64@0.4.0: {} + + node-releases@2.0.51: {} + + nodemailer@9.0.3: {} + + nodemon@3.1.14: + dependencies: + chokidar: 3.6.0 + debug: 4.4.3(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 10.2.5 + pstree.remy: 1.1.8 + semver: 7.8.5 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.1 + undefsafe: 2.0.5 + + normalize-path@3.0.0: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + ohash@2.0.11: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + pako@0.2.9: {} + + pako@1.0.11: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.7 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parseurl@1.3.3: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + path-to-regexp@8.4.2: {} + + pathe@2.0.3: {} + + pdfkit@0.19.1: + dependencies: + '@noble/ciphers': 1.3.0 + '@noble/hashes': 1.8.0 + fontkit: 2.0.4 + js-md5: 0.8.3 + linebreak: 1.1.0 + png-js: 1.1.0 + + perfect-debounce@2.1.0: {} + + pg-cloudflare@1.4.0: + optional: true + + pg-connection-string@2.14.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.22.0): + dependencies: + pg: 8.22.0 + + pg-protocol@1.15.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.22.0: + dependencies: + pg-connection-string: 2.14.0 + pg-pool: 3.14.0(pg@8.22.0) + pg-protocol: 1.15.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.5: {} + + pirates@4.0.7: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + pkg-types@2.3.1: + dependencies: + confbox: 0.2.4 + exsolve: 1.1.0 + pathe: 2.0.3 + + png-js@1.1.0: + dependencies: + browserify-zlib: 0.2.0 + + possible-typed-array-names@1.1.0: {} + + postal-mime@2.7.5: {} + + postgres-array@2.0.0: {} + + postgres-array@3.0.4: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + postgres@3.4.7: {} + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.1: + dependencies: + fast-diff: 1.3.0 + + prettier@3.9.6: {} + + pretty-format@30.4.1: + dependencies: + '@jest/schemas': 30.4.1 + ansi-styles: 5.2.0 + react-is-18: react-is@18.3.1 + react-is-19: react-is@19.2.8 + + prisma@7.9.0(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3): + dependencies: + '@prisma/config': 7.9.0 + '@prisma/dev': 0.24.14(typescript@5.9.3) + '@prisma/engines': 7.9.0 + '@prisma/studio-core': 0.33.0(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + mysql2: 3.15.3 + postgres: 3.4.7 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + - magicast + - react + - react-dom + + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-from-env@2.1.0: {} + + pstree.remy@1.1.8: {} + + punycode@2.3.1: {} + + pure-rand@6.1.0: {} + + pure-rand@7.0.1: {} + + qs@6.15.3: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + range-parser@1.3.0: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + unpipe: 1.0.0 + + rc9@3.0.1: + dependencies: + defu: 6.1.7 + destr: 2.0.5 + + react-dom@19.2.8(react@19.2.8): + dependencies: + react: 19.2.8 + scheduler: 0.27.0 + + react-is@18.3.1: {} + + react-is@19.2.8: {} + + react@19.2.8: {} + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + + readdirp@5.0.0: {} + + reflect-metadata@0.2.2: {} + + remeda@2.33.4: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resend@6.18.0: + dependencies: + postal-mime: 2.7.5 + standardwebhooks: 1.0.0 + + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + restructure@3.0.2: {} + + ret@0.5.0: {} + + retry@0.12.0: {} + + robust-predicates@3.0.3: {} + + router@2.2.0: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + safe-buffer@5.2.1: {} + + safe-regex2@5.1.1: + dependencies: + ret: 0.5.0 + + safer-buffer@2.1.2: {} + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + semver@7.8.5: {} + + send@1.2.1: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.3.0 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + seq-queue@0.0.5: {} + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + setprototypeof@1.2.0: {} + + sha.js@2.4.12: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + simple-update-notifier@2.0.0: + dependencies: + semver: 7.8.5 + + slash@3.0.0: {} + + source-map-support@0.5.13: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + split2@4.2.0: {} + + sprintf-js@1.0.3: {} + + sql-highlight@6.1.0: {} + + sqlstring@2.3.3: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + standardwebhooks@1.0.0: + dependencies: + '@stablelib/base64': 1.0.1 + fast-sha256: 1.3.0 + + statuses@2.0.2: {} + + std-env@3.10.0: {} + + string-length@4.0.2: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@4.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-json-comments@3.1.1: {} + + superagent@10.3.0: + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.4.3(supports-color@5.5.0) + fast-safe-stringify: 2.1.1 + form-data: 4.0.6 + formidable: 3.5.4 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.15.3 + transitivePeerDependencies: + - supports-color + + supertest@7.2.2: + dependencies: + cookie-signature: 1.2.2 + methods: 1.1.2 + superagent: 10.3.0 + transitivePeerDependencies: + - supports-color + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + synckit@0.11.13: + dependencies: + '@pkgr/core': 0.3.6 + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.6 + glob: 7.2.3 + minimatch: 3.1.5 + + tiny-inflate@1.0.3: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tmpl@1.0.5: {} + + to-buffer@1.2.2: + dependencies: + isarray: 2.0.5 + safe-buffer: 5.2.1 + typed-array-buffer: 1.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + toml@3.0.0: {} + + touch@3.1.1: {} + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-essentials@10.2.1(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + + ts-jest@29.4.12(@babel/core@7.29.7)(@jest/transform@30.4.1)(@jest/types@30.4.1)(babel-jest@30.4.1(@babel/core@7.29.7))(jest-util@30.4.1)(jest@30.4.2(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.9.3)))(typescript@5.9.3): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.9 + jest: 30.4.2(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.9.3)) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.8.5 + type-fest: 4.41.0 + typescript: 5.9.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.29.7 + '@jest/transform': 30.4.1 + '@jest/types': 30.4.1 + babel-jest: 30.4.1(@babel/core@7.29.7) + jest-util: 30.4.1 + + ts-node@10.9.2(@types/node@22.20.1)(typescript@5.9.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.20.1 + acorn: 8.17.0 + acorn-walk: 8.3.5 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.4 + make-error: 1.3.6 + typescript: 5.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + tslib@2.8.1: {} + + tsx@4.23.1: + dependencies: + esbuild: 0.28.1 + optionalDependencies: + fsevents: 2.3.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@4.0.8: {} + + type-fest@0.21.3: {} + + type-fest@4.41.0: {} + + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.1 + mime-types: 3.0.2 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typeorm@0.3.31(mysql2@3.15.3)(pg@8.22.0)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.9.3)): + dependencies: + '@sqltools/formatter': 1.2.5 + ansis: 4.3.1 + app-root-path: 3.1.0 + buffer: 6.0.3 + dayjs: 1.11.21 + debug: 4.4.3(supports-color@5.5.0) + dedent: 1.7.2 + dotenv: 16.6.1 + glob: 10.5.0 + reflect-metadata: 0.2.2 + sha.js: 2.4.12 + sql-highlight: 6.1.0 + tslib: 2.8.1 + uuid: 11.1.1 + yargs: 17.7.3 + optionalDependencies: + mysql2: 3.15.3 + pg: 8.22.0 + ts-node: 10.9.2(@types/node@22.20.1)(typescript@5.9.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + typescript-eslint@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint: 9.39.5(jiti@2.7.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + uglify-js@3.19.3: + optional: true + + undefsafe@2.0.5: {} + + undici-types@6.21.0: {} + + unicode-properties@1.4.1: + dependencies: + base64-js: 1.5.1 + unicode-trie: 2.0.0 + + unicode-trie@2.0.0: + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + + unpipe@1.0.0: {} + + unrs-resolver@1.12.2: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.12.2 + '@unrs/resolver-binding-android-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-x64': 1.12.2 + '@unrs/resolver-binding-freebsd-x64': 1.12.2 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-arm64-musl': 1.12.2 + '@unrs/resolver-binding-linux-loong64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-loong64-musl': 1.12.2 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-musl': 1.12.2 + '@unrs/resolver-binding-linux-s390x-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-musl': 1.12.2 + '@unrs/resolver-binding-openharmony-arm64': 1.12.2 + '@unrs/resolver-binding-wasm32-wasi': 1.12.2 + '@unrs/resolver-binding-win32-arm64-msvc': 1.12.2 + '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 + '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 + + update-browserslist-db@1.2.3(browserslist@4.28.7): + dependencies: + browserslist: 4.28.7 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + urijs@1.19.11: {} + + uuid@11.1.1: {} + + v8-compile-cache-lib@3.0.1: {} + + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + + valibot@1.2.0(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + + vary@1.1.2: {} + + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + which-typed-array@1.1.22: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wordwrap@1.0.0: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + wrappy@1.0.2: {} + + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + xtend@4.0.2: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yn@3.1.1: {} + + yocto-queue@0.1.0: {} + + zeptomatch@2.1.0: + dependencies: + grammex: 3.1.13 + graphmatch: 1.1.1 diff --git a/prisma.config.js b/prisma.config.js new file mode 100644 index 0000000..3eb6195 --- /dev/null +++ b/prisma.config.js @@ -0,0 +1,13 @@ +// This file was generated by Prisma, and assumes you have installed the following: +// npm install --save-dev prisma dotenv +import 'dotenv/config'; +import { defineConfig } from 'prisma/config'; +export default defineConfig({ + schema: 'prisma/schema.prisma', + migrations: { + path: 'prisma/migrations', + }, + datasource: { + url: process.env['DATABASE_URL'], + }, +}); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7f95ef2..c8ba683 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -244,3 +244,17 @@ model PaymentConfirmation { idempotencyKey String @unique createdAt DateTime @default(now()) } + +model IndexerCursor { + id String @id @default(uuid()) + contractId String @unique + lastLedger Int + updatedAt DateTime @updatedAt +} + +model IndexerEvent { + id String @id + topic String + ledger Int + processedAt DateTime @default(now()) +} diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..4b7c76a --- /dev/null +++ b/src/app.js @@ -0,0 +1,14 @@ +import 'reflect-metadata'; +import express from 'express'; +import cors from 'cors'; +import helmet from 'helmet'; +import routes from './routes/index.js'; +const app = express(); +// Middleware +app.use(helmet()); +app.use(cors()); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +// Routes +app.use('/api/v1/', routes); +export default app; diff --git a/src/config/database.js b/src/config/database.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/src/config/database.js @@ -0,0 +1 @@ +export {}; diff --git a/src/config/environment.js b/src/config/environment.js new file mode 100644 index 0000000..d825fa5 --- /dev/null +++ b/src/config/environment.js @@ -0,0 +1,51 @@ +import dotenv from 'dotenv'; +import path from 'path'; +import { fileURLToPath } from 'url'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +// Load environment variables from .env file +dotenv.config({ path: path.join(__dirname, '../../.env') }); +const EMAIL_PROVIDERS = ['console', 'resend', 'smtp']; +const parseEmailProvider = (value) => { + const provider = value || 'console'; + if (!EMAIL_PROVIDERS.includes(provider)) { + console.warn(`Invalid EMAIL_PROVIDER "${provider}", falling back to console`); + return 'console'; + } + return provider; +}; +const parseOptionalInt = (value) => { + if (!value || value.trim() === '') + return undefined; + const parsed = parseInt(value, 10); + return Number.isNaN(parsed) ? undefined : parsed; +}; +export const environment = { + nodeEnv: process.env.NODE_ENV || 'development', + port: parseInt(process.env.PORT || '3000', 10), + jwtSecret: process.env.JWT_SECRET || 'dev-jwt-secret-change-in-production', + db: { + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT || '5432', 10), + username: process.env.DB_USERNAME || 'postgres', + password: process.env.DB_PASSWORD || 'postgres', + database: process.env.DB_DATABASE || 'postgres', + }, + email: { + from: process.env.EMAIL_FROM || 'noreply@shade.local', + provider: parseEmailProvider(process.env.EMAIL_PROVIDER), + resendApiKey: process.env.RESEND_API_KEY || '', + smtp: { + host: process.env.SMTP_HOST || '', + port: parseInt(process.env.SMTP_PORT || '587', 10), + user: process.env.SMTP_USER || '', + pass: process.env.SMTP_PASS || '', + secure: process.env.SMTP_SECURE === 'true', + }, + }, + stellar: { + rpcUrl: process.env.STELLAR_RPC_URL || 'https://soroban-testnet.stellar.org', + contractId: process.env.STELLAR_CONTRACT_ID || '', + indexerStartLedger: parseOptionalInt(process.env.STELLAR_INDEXER_START_LEDGER), + }, +}; diff --git a/src/config/environment.ts b/src/config/environment.ts index acf7d77..8e60da5 100644 --- a/src/config/environment.ts +++ b/src/config/environment.ts @@ -21,6 +21,12 @@ const parseEmailProvider = (value: string | undefined): EmailProvider => { return provider as EmailProvider; }; +const parseOptionalInt = (value: string | undefined): number | undefined => { + if (!value || value.trim() === '') return undefined; + const parsed = parseInt(value, 10); + return Number.isNaN(parsed) ? undefined : parsed; +}; + export const environment = { nodeEnv: process.env.NODE_ENV || 'development', port: parseInt(process.env.PORT || '3000', 10), @@ -44,4 +50,9 @@ export const environment = { secure: process.env.SMTP_SECURE === 'true', }, }, + stellar: { + rpcUrl: process.env.STELLAR_RPC_URL || 'https://soroban-testnet.stellar.org', + contractId: process.env.STELLAR_CONTRACT_ID || '', + indexerStartLedger: parseOptionalInt(process.env.STELLAR_INDEXER_START_LEDGER), + }, }; diff --git a/src/config/prisma.js b/src/config/prisma.js new file mode 100644 index 0000000..9703922 --- /dev/null +++ b/src/config/prisma.js @@ -0,0 +1,18 @@ +import dotenv from 'dotenv'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { PrismaClient } from '@prisma/client'; +import { PrismaPg } from '@prisma/adapter-pg'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +dotenv.config({ path: path.join(__dirname, '../../.env') }); +const prismaClientSingleton = () => { + if (process.env.NODE_ENV === 'test') + return {}; + const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); + return new PrismaClient({ adapter }); +}; +const prisma = globalThis.prisma ?? prismaClientSingleton(); +export default prisma; +if (process.env.NODE_ENV !== 'production') + globalThis.prisma = prisma; diff --git a/src/controllers/api-key.controllers.js b/src/controllers/api-key.controllers.js new file mode 100644 index 0000000..b2a419e --- /dev/null +++ b/src/controllers/api-key.controllers.js @@ -0,0 +1,57 @@ +import { createApiKey, listApiKeys, revokeApiKey } from '../services/api-key.services.js'; +import { AppError } from '../utils/errors.js'; +export const createApiKeyController = async (req, res) => { + const merchant = req.merchant; + if (!merchant) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + if (req.body?.label !== undefined && typeof req.body.label !== 'string') { + res.status(400).json({ error: 'label must be a string' }); + return; + } + const label = typeof req.body?.label === 'string' ? req.body.label : undefined; + try { + const apiKey = await createApiKey(merchant.id, label); + res.status(201).json(apiKey); + } + catch (error) { + if (error instanceof AppError) { + res.status(error.statusCode).json({ error: error.message }); + return; + } + res.status(500).json({ error: 'Internal Server Error' }); + } +}; +export const listApiKeysController = async (req, res) => { + const merchant = req.merchant; + if (!merchant) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + try { + const apiKeys = await listApiKeys(merchant.id); + res.status(200).json(apiKeys); + } + catch { + res.status(500).json({ error: 'Internal Server Error' }); + } +}; +export const revokeApiKeyController = async (req, res) => { + const merchant = req.merchant; + if (!merchant) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + try { + await revokeApiKey(merchant.id, req.params.id); + res.status(200).json({ message: 'API key revoked' }); + } + catch (error) { + if (error instanceof AppError) { + res.status(error.statusCode).json({ error: error.message }); + return; + } + res.status(500).json({ error: 'Internal Server Error' }); + } +}; diff --git a/src/controllers/api-key.controllers.ts b/src/controllers/api-key.controllers.ts index 32f97da..a5df44e 100644 --- a/src/controllers/api-key.controllers.ts +++ b/src/controllers/api-key.controllers.ts @@ -54,7 +54,7 @@ export const revokeApiKeyController = async (req: Request, res: Response): Promi } try { - await revokeApiKey(merchant.id, req.params.id); + await revokeApiKey(merchant.id, req.params.id as string); res.status(200).json({ message: 'API key revoked' }); } catch (error) { if (error instanceof AppError) { diff --git a/src/controllers/auth.controllers.js b/src/controllers/auth.controllers.js new file mode 100644 index 0000000..813d6b7 --- /dev/null +++ b/src/controllers/auth.controllers.js @@ -0,0 +1,85 @@ +import { createNonce, authenticateWallet } from '../services/auth.services.js'; +import { resendEmailOtp, verifyEmailOtp } from '../services/otp.services.js'; +import { sanitizeMerchant } from '../services/merchant.services.js'; +import { AppError } from '../utils/errors.js'; +export const createNonceController = async (req, res) => { + try { + const { address } = req.body; + if (!address || typeof address !== 'string') { + res.status(400).json({ error: 'address is required' }); + return; + } + const result = await createNonce(address); + res.status(201).json(result); + } + catch (error) { + res.status(500).json({ error: 'Internal Server Error' }); + } +}; +export const verifySignatureController = async (req, res) => { + try { + const { address, nonce, signature } = req.body; + if (!address || !nonce || !signature) { + res.status(400).json({ error: 'address, nonce, and signature are required' }); + return; + } + if (typeof address !== 'string' || typeof nonce !== 'string' || typeof signature !== 'string') { + res.status(400).json({ error: 'address, nonce, and signature must be strings' }); + return; + } + const result = await authenticateWallet(address, nonce, signature); + if (!result.success) { + res.status(401).json({ error: result.reason }); + return; + } + res.status(200).json({ + accessToken: result.accessToken, + refreshToken: result.refreshToken, + merchant: result.merchant, + }); + } + catch (error) { + res.status(500).json({ error: 'Internal Server Error' }); + } +}; +export const verifyEmailController = async (req, res) => { + const merchant = req.merchant; + if (!merchant) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + const { code } = req.body; + if (!code || typeof code !== 'string') { + res.status(400).json({ error: 'code is required' }); + return; + } + try { + const updatedMerchant = await verifyEmailOtp(merchant.id, code.trim()); + res.status(200).json(sanitizeMerchant(updatedMerchant)); + } + catch (error) { + if (error instanceof AppError) { + res.status(error.statusCode).json({ error: error.message }); + return; + } + res.status(500).json({ error: 'Internal Server Error' }); + } +}; +export const resendOtpController = async (req, res) => { + const merchant = req.merchant; + if (!merchant) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + try { + await resendEmailOtp(merchant.id); + res.status(200).json({ message: 'Verification code sent' }); + } + catch (error) { + if (error instanceof AppError) { + res.status(error.statusCode).json({ error: error.message }); + return; + } + res.status(500).json({ error: 'Internal Server Error' }); + } +}; diff --git a/src/controllers/index.js b/src/controllers/index.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/src/controllers/index.js @@ -0,0 +1 @@ +export {}; diff --git a/src/controllers/invoice.controllers.js b/src/controllers/invoice.controllers.js new file mode 100644 index 0000000..b00ee5f --- /dev/null +++ b/src/controllers/invoice.controllers.js @@ -0,0 +1,127 @@ +import { createInvoice, getInvoice, getInvoiceWithMerchant, listInvoices, voidInvoice, } from '../services/invoice.services.js'; +import { parseInvoiceListQuery, validateCreateInvoice } from '../utils/invoice.validation.js'; +import { AppError } from '../utils/errors.js'; +import { generateInvoicePdf } from '../services/invoice-pdf.services.js'; +import { sendInvoiceEmail } from '../services/email.service.js'; +export const createInvoiceController = async (req, res) => { + const merchant = req.merchant; + if (!merchant) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + const errors = validateCreateInvoice(req.body); + if (Object.keys(errors).length > 0) { + res.status(400).json({ error: 'Validation failed', errors }); + return; + } + try { + const invoice = await createInvoice(merchant.id, req.body); + res.status(201).json(invoice); + } + catch (error) { + if (error instanceof AppError) { + res.status(error.statusCode).json({ error: error.message }); + return; + } + res.status(500).json({ error: 'Internal Server Error' }); + } +}; +export const listInvoicesController = async (req, res) => { + const merchant = req.merchant; + if (!merchant) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + const { filters, pagination, errors } = parseInvoiceListQuery(req.query); + if (Object.keys(errors).length > 0) { + res.status(400).json({ error: 'Validation failed', errors }); + return; + } + try { + const result = await listInvoices(merchant.id, filters, pagination); + res.status(200).json(result); + } + catch { + res.status(500).json({ error: 'Internal Server Error' }); + } +}; +export const getInvoiceController = async (req, res) => { + const merchant = req.merchant; + if (!merchant) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + try { + const invoice = await getInvoice(merchant.id, req.params.id); + res.status(200).json(invoice); + } + catch (error) { + if (error instanceof AppError) { + res.status(error.statusCode).json({ error: error.message }); + return; + } + res.status(500).json({ error: 'Internal Server Error' }); + } +}; +export const voidInvoiceController = async (req, res) => { + const merchant = req.merchant; + if (!merchant) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + try { + const invoice = await voidInvoice(merchant.id, req.params.id); + res.status(200).json(invoice); + } + catch (error) { + if (error instanceof AppError) { + res.status(error.statusCode).json({ error: error.message }); + return; + } + res.status(500).json({ error: 'Internal Server Error' }); + } +}; +export const getInvoicePdfController = async (req, res) => { + const merchant = req.merchant; + if (!merchant) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + try { + const invoice = await getInvoiceWithMerchant(merchant.id, req.params.id); + const pdf = await generateInvoicePdf(invoice, invoice.merchant); + res.setHeader('Content-Type', 'application/pdf'); + res.setHeader('Content-Disposition', `attachment; filename="invoice-${invoice.paymentSlug}.pdf"`); + res.status(200).send(pdf); + } + catch (error) { + if (error instanceof AppError) { + res.status(error.statusCode).json({ error: error.message }); + return; + } + res.status(500).json({ error: 'Internal Server Error' }); + } +}; +export const sendInvoiceController = async (req, res) => { + const merchant = req.merchant; + if (!merchant) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + try { + const invoice = await getInvoiceWithMerchant(merchant.id, req.params.id); + if (!invoice.email) { + res.status(400).json({ error: 'Invoice has no email on file' }); + return; + } + await sendInvoiceEmail(invoice, invoice.merchant); + res.status(200).json({ message: 'Invoice email sent' }); + } + catch (error) { + if (error instanceof AppError) { + res.status(error.statusCode).json({ error: error.message }); + return; + } + res.status(500).json({ error: 'Internal Server Error' }); + } +}; diff --git a/src/controllers/invoice.controllers.ts b/src/controllers/invoice.controllers.ts index 880dc91..d304994 100644 --- a/src/controllers/invoice.controllers.ts +++ b/src/controllers/invoice.controllers.ts @@ -67,7 +67,7 @@ export const getInvoiceController = async (req: Request, res: Response): Promise } try { - const invoice = await getInvoice(merchant.id, req.params.id); + const invoice = await getInvoice(merchant.id, req.params.id as string); res.status(200).json(invoice); } catch (error) { if (error instanceof AppError) { @@ -86,7 +86,7 @@ export const voidInvoiceController = async (req: Request, res: Response): Promis } try { - const invoice = await voidInvoice(merchant.id, req.params.id); + const invoice = await voidInvoice(merchant.id, req.params.id as string); res.status(200).json(invoice); } catch (error) { if (error instanceof AppError) { @@ -105,7 +105,7 @@ export const getInvoicePdfController = async (req: Request, res: Response): Prom } try { - const invoice = await getInvoiceWithMerchant(merchant.id, req.params.id); + const invoice = await getInvoiceWithMerchant(merchant.id, req.params.id as string); const pdf = await generateInvoicePdf(invoice, invoice.merchant); res.setHeader('Content-Type', 'application/pdf'); @@ -131,7 +131,7 @@ export const sendInvoiceController = async (req: Request, res: Response): Promis } try { - const invoice = await getInvoiceWithMerchant(merchant.id, req.params.id); + const invoice = await getInvoiceWithMerchant(merchant.id, req.params.id as string); if (!invoice.email) { res.status(400).json({ error: 'Invoice has no email on file' }); diff --git a/src/controllers/merchant.controllers.js b/src/controllers/merchant.controllers.js new file mode 100644 index 0000000..3c34600 --- /dev/null +++ b/src/controllers/merchant.controllers.js @@ -0,0 +1,112 @@ +import { createMerchant, getMerchant, listMerchants, registerMerchant, getMyProfile, updateMyProfile, generateMerchantSigningKey, } from '../services/merchant.services.js'; +import { validateRegisterMerchant, validateUpdateMerchant } from '../utils/validation.js'; +import { AppError } from '../utils/errors.js'; +export const createMerchantController = async (req, res) => { + try { + const merchant = await createMerchant(req.body); + res.status(201).json(merchant); + } + catch (error) { + res.status(500).json({ error: 'Internal Server Error' }); + } +}; +export const getMerchantController = async (req, res) => { + try { + const merchant = await getMerchant(Number(req.params.id)); + res.status(200).json(merchant); + } + catch (error) { + res.status(500).json({ error: 'Internal Server Error' }); + } +}; +export const listMerchantsController = async (req, res) => { + try { + const merchants = await listMerchants(Number(req.query.limit), Number(req.query.offset)); + res.status(200).json(merchants); + } + catch (error) { + res.status(500).json({ error: 'Internal Server Error' }); + } +}; +export const registerMerchantController = async (req, res) => { + const merchant = req.merchant; + if (!merchant) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + const errors = validateRegisterMerchant(req.body); + if (Object.keys(errors).length > 0) { + res.status(400).json({ error: 'Validation failed', errors }); + return; + } + try { + const profile = await registerMerchant(merchant.id, req.body); + res.status(200).json(profile); + } + catch (error) { + if (error instanceof AppError) { + res.status(error.statusCode).json({ error: error.message }); + return; + } + res.status(500).json({ error: 'Internal Server Error' }); + } +}; +export const getMyProfileController = async (req, res) => { + const merchant = req.merchant; + if (!merchant) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + try { + const profile = await getMyProfile(merchant.id); + res.status(200).json(profile); + } + catch (error) { + if (error instanceof AppError) { + res.status(error.statusCode).json({ error: error.message }); + return; + } + res.status(500).json({ error: 'Internal Server Error' }); + } +}; +export const generateSigningKeyController = async (req, res) => { + const merchant = req.merchant; + if (!merchant) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + try { + const keys = await generateMerchantSigningKey(merchant.id); + res.status(201).json(keys); + } + catch (error) { + if (error instanceof AppError) { + res.status(error.statusCode).json({ error: error.message }); + return; + } + res.status(500).json({ error: 'Internal Server Error' }); + } +}; +export const updateMyProfileController = async (req, res) => { + const merchant = req.merchant; + if (!merchant) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + const errors = validateUpdateMerchant(req.body); + if (Object.keys(errors).length > 0) { + res.status(400).json({ error: 'Validation failed', errors }); + return; + } + try { + const profile = await updateMyProfile(merchant.id, req.body); + res.status(200).json(profile); + } + catch (error) { + if (error instanceof AppError) { + res.status(error.statusCode).json({ error: error.message }); + return; + } + res.status(500).json({ error: 'Internal Server Error' }); + } +}; diff --git a/src/controllers/pay.controllers.js b/src/controllers/pay.controllers.js new file mode 100644 index 0000000..78836c7 --- /dev/null +++ b/src/controllers/pay.controllers.js @@ -0,0 +1,69 @@ +import { resolveInvoiceBySlug, confirmPayment, getInvoiceForPdfBySlug, } from '../services/pay.services.js'; +import { AppError } from '../utils/errors.js'; +import { generateInvoicePdf } from '../services/invoice-pdf.services.js'; +export const resolveInvoiceController = async (req, res) => { + try { + const { slug } = req.params; + const invoice = await resolveInvoiceBySlug(slug); + res.status(200).json(invoice); + } + catch (error) { + if (error instanceof AppError) { + if (error.statusCode === 410 && error.message === 'expired') { + res.status(410).json({ reason: 'expired' }); + return; + } + res.status(error.statusCode).json({ error: error.message }); + return; + } + res.status(500).json({ error: 'Internal Server Error' }); + } +}; +export const getInvoicePdfController = async (req, res) => { + try { + const { slug } = req.params; + const invoice = await getInvoiceForPdfBySlug(slug); + const pdf = await generateInvoicePdf(invoice, invoice.merchant); + res.setHeader('Content-Type', 'application/pdf'); + res.setHeader('Content-Disposition', `attachment; filename="invoice-${invoice.paymentSlug}.pdf"`); + res.status(200).send(pdf); + } + catch (error) { + if (error instanceof AppError) { + if (error.statusCode === 410 && error.message === 'expired') { + res.status(410).json({ reason: 'expired' }); + return; + } + res.status(error.statusCode).json({ error: error.message }); + return; + } + res.status(500).json({ error: 'Internal Server Error' }); + } +}; +export const confirmPaymentController = async (req, res) => { + try { + const { slug } = req.params; + const { payerAddress, txHash } = req.body; + if (!payerAddress || typeof payerAddress !== 'string') { + res.status(400).json({ error: 'payerAddress is required and must be a string' }); + return; + } + if (txHash !== undefined && typeof txHash !== 'string') { + res.status(400).json({ error: 'txHash must be a string if provided' }); + return; + } + await confirmPayment(slug, payerAddress, txHash); + res.status(202).json({ message: 'Payment confirmation received' }); + } + catch (error) { + if (error instanceof AppError) { + if (error.statusCode === 410 && error.message === 'expired') { + res.status(410).json({ reason: 'expired' }); + return; + } + res.status(error.statusCode).json({ error: error.message }); + return; + } + res.status(500).json({ error: 'Internal Server Error' }); + } +}; diff --git a/src/controllers/pay.controllers.ts b/src/controllers/pay.controllers.ts index ab0cf30..0b88f74 100644 --- a/src/controllers/pay.controllers.ts +++ b/src/controllers/pay.controllers.ts @@ -10,7 +10,7 @@ import { generateInvoicePdf } from '../services/invoice-pdf.services.js'; export const resolveInvoiceController = async (req: Request, res: Response): Promise => { try { const { slug } = req.params; - const invoice = await resolveInvoiceBySlug(slug); + const invoice = await resolveInvoiceBySlug(slug as string); res.status(200).json(invoice); } catch (error) { if (error instanceof AppError) { @@ -28,7 +28,7 @@ export const resolveInvoiceController = async (req: Request, res: Response): Pro export const getInvoicePdfController = async (req: Request, res: Response): Promise => { try { const { slug } = req.params; - const invoice = await getInvoiceForPdfBySlug(slug); + const invoice = await getInvoiceForPdfBySlug(slug as string); const pdf = await generateInvoicePdf(invoice, invoice.merchant); res.setHeader('Content-Type', 'application/pdf'); @@ -65,7 +65,7 @@ export const confirmPaymentController = async (req: Request, res: Response): Pro return; } - await confirmPayment(slug, payerAddress, txHash); + await confirmPayment(slug as string, payerAddress, txHash); res.status(202).json({ message: 'Payment confirmation received' }); } catch (error) { if (error instanceof AppError) { diff --git a/src/entities/index.js b/src/entities/index.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/src/entities/index.js @@ -0,0 +1 @@ +export {}; diff --git a/src/indexer/handlers/index.js b/src/indexer/handlers/index.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/src/indexer/handlers/index.js @@ -0,0 +1 @@ +export {}; diff --git a/src/indexer/handlers/index.ts b/src/indexer/handlers/index.ts new file mode 100644 index 0000000..4d39919 --- /dev/null +++ b/src/indexer/handlers/index.ts @@ -0,0 +1,2 @@ +// Handlers will be registered here as separate issues are implemented. +export {}; diff --git a/src/indexer/poller.js b/src/indexer/poller.js new file mode 100644 index 0000000..bda7fdd --- /dev/null +++ b/src/indexer/poller.js @@ -0,0 +1,148 @@ +import { scValToNative } from '@stellar/stellar-sdk'; +import prisma from '../config/prisma.js'; +import { environment } from '../config/environment.js'; +import { sorobanServer } from './sorobanClient.js'; +import { dispatch } from './registry.js'; +let isRunning = false; +let cursor; +function decodeTopic(val) { + if (!val) + return ''; + try { + const native = scValToNative(val); + if (typeof native === 'symbol') { + return native.description ?? native.toString(); + } + return String(native); + } + catch { + return 'unknown_topic'; + } +} +export async function tick() { + try { + const contractId = environment.stellar.contractId; + if (!contractId || contractId.trim() === '') { + throw new Error('STELLAR_CONTRACT_ID environment variable is unset or empty'); + } + const latestLedgerResp = await sorobanServer.getLatestLedger(); + const latestLedger = latestLedgerResp.sequence; + if (cursor === undefined) { + const cursorRecord = await prisma.indexerCursor.findUnique({ + where: { contractId }, + }); + if (cursorRecord?.lastLedger != null) { + cursor = cursorRecord.lastLedger; + } + else if (environment.stellar.indexerStartLedger != null) { + cursor = environment.stellar.indexerStartLedger; + } + else { + cursor = latestLedger; + } + console.log(`Indexer initialized with cursor at ledger ${cursor}`); + } + const currentCursor = cursor ?? latestLedger; + cursor = currentCursor; + if (currentCursor > latestLedger) { + return; + } + const eventsResp = await sorobanServer.getEvents({ + startLedger: currentCursor, + filters: [{ type: 'contract', contractIds: [contractId] }], + limit: 100, + }); + const events = eventsResp.events || []; + const processedIds = []; + for (const event of events) { + try { + const existing = await prisma.indexerEvent.findUnique({ + where: { id: event.id }, + }); + if (existing) { + continue; + } + const topicVal = event.topic && event.topic.length > 0 ? event.topic[0] : undefined; + const decodedTopic = decodeTopic(topicVal); + let decodedValue = null; + try { + decodedValue = event.value ? scValToNative(event.value) : null; + } + catch { + decodedValue = null; + } + console.log(`Decoded event [${event.id}] - topic: ${decodedTopic}, value:`, decodedValue); + await dispatch({ + id: event.id, + topic: decodedTopic, + ledger: event.ledger, + txHash: event.txHash, + data: decodedValue, + }); + processedIds.push({ + id: event.id, + topic: decodedTopic, + ledger: event.ledger, + }); + } + catch (err) { + console.error(`Error processing event ${event.id}:`, err); + } + } + const nextCursor = events.length === 100 && events[events.length - 1] + ? events[events.length - 1].ledger + 1 + : latestLedger + 1; + await prisma.$transaction(async (tx) => { + for (const item of processedIds) { + await tx.indexerEvent.create({ + data: { + id: item.id, + topic: item.topic, + ledger: item.ledger, + }, + }); + } + await tx.indexerCursor.upsert({ + where: { contractId }, + update: { lastLedger: nextCursor }, + create: { contractId, lastLedger: nextCursor }, + }); + }); + cursor = nextCursor; + } + catch (error) { + console.error('Error in poller tick:', error); + if (!environment.stellar.contractId || environment.stellar.contractId.trim() === '') { + throw error; + } + } +} +export async function startPolling(intervalMs = 6000) { + const contractId = environment.stellar.contractId; + if (!contractId || contractId.trim() === '') { + throw new Error('STELLAR_CONTRACT_ID environment variable is unset or empty'); + } + if (isRunning) + return; + isRunning = true; + console.log(`Starting Soroban indexer poller for contract ${contractId}...`); + while (isRunning) { + await tick(); + if (!isRunning) + break; + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + } +} +export function stopPolling() { + isRunning = false; +} +export function getCursor() { + return cursor; +} +export function setCursor(val) { + cursor = val; +} +export function resetPoller() { + stopPolling(); + cursor = undefined; +} diff --git a/src/indexer/poller.ts b/src/indexer/poller.ts new file mode 100644 index 0000000..db2f184 --- /dev/null +++ b/src/indexer/poller.ts @@ -0,0 +1,163 @@ +import { scValToNative } from '@stellar/stellar-sdk'; +import prisma from '../config/prisma.js'; +import { environment } from '../config/environment.js'; +import { sorobanServer } from './sorobanClient.js'; +import { dispatch } from './registry.js'; + +let isRunning = false; +let cursor: number | undefined; + +function decodeTopic(val: any): string { + if (!val) return ''; + try { + const native = scValToNative(val); + if (typeof native === 'symbol') { + return native.description ?? native.toString(); + } + return String(native); + } catch { + return 'unknown_topic'; + } +} + +export async function tick(): Promise { + try { + const contractId = environment.stellar.contractId; + if (!contractId || contractId.trim() === '') { + throw new Error('STELLAR_CONTRACT_ID environment variable is unset or empty'); + } + + const latestLedgerResp = await sorobanServer.getLatestLedger(); + const latestLedger = latestLedgerResp.sequence; + + if (cursor === undefined) { + const cursorRecord = await prisma.indexerCursor.findUnique({ + where: { contractId }, + }); + if (cursorRecord?.lastLedger != null) { + cursor = cursorRecord.lastLedger; + } else if (environment.stellar.indexerStartLedger != null) { + cursor = environment.stellar.indexerStartLedger; + } else { + cursor = latestLedger; + } + console.log(`Indexer initialized with cursor at ledger ${cursor}`); + } + + const currentCursor = cursor ?? latestLedger; + cursor = currentCursor; + + if (currentCursor > latestLedger) { + return; + } + + const eventsResp = await sorobanServer.getEvents({ + startLedger: currentCursor, + filters: [{ type: 'contract', contractIds: [contractId] }], + limit: 100, + }); + + const events = eventsResp.events || []; + const processedIds: { id: string; topic: string; ledger: number }[] = []; + + for (const event of events) { + try { + const existing = await prisma.indexerEvent.findUnique({ + where: { id: event.id }, + }); + if (existing) { + continue; + } + + const topicVal = event.topic && event.topic.length > 0 ? event.topic[0] : undefined; + const decodedTopic = decodeTopic(topicVal); + let decodedValue: any = null; + try { + decodedValue = event.value ? scValToNative(event.value) : null; + } catch { + decodedValue = null; + } + + console.log(`Decoded event [${event.id}] - topic: ${decodedTopic}, value:`, decodedValue); + + await dispatch({ + id: event.id, + topic: decodedTopic, + ledger: event.ledger, + txHash: event.txHash, + data: decodedValue, + }); + + processedIds.push({ + id: event.id, + topic: decodedTopic, + ledger: event.ledger, + }); + } catch (err) { + console.error(`Error processing event ${event.id}:`, err); + } + } + + const nextCursor = + events.length === 100 && events[events.length - 1] + ? events[events.length - 1].ledger + 1 + : latestLedger + 1; + + await prisma.$transaction(async (tx) => { + for (const item of processedIds) { + await tx.indexerEvent.create({ + data: { + id: item.id, + topic: item.topic, + ledger: item.ledger, + }, + }); + } + await tx.indexerCursor.upsert({ + where: { contractId }, + update: { lastLedger: nextCursor }, + create: { contractId, lastLedger: nextCursor }, + }); + }); + + cursor = nextCursor; + } catch (error) { + console.error('Error in poller tick:', error); + if (!environment.stellar.contractId || environment.stellar.contractId.trim() === '') { + throw error; + } + } +} + +export async function startPolling(intervalMs = 6000): Promise { + const contractId = environment.stellar.contractId; + if (!contractId || contractId.trim() === '') { + throw new Error('STELLAR_CONTRACT_ID environment variable is unset or empty'); + } + if (isRunning) return; + isRunning = true; + console.log(`Starting Soroban indexer poller for contract ${contractId}...`); + + while (isRunning) { + await tick(); + if (!isRunning) break; + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + } +} + +export function stopPolling(): void { + isRunning = false; +} + +export function getCursor(): number | undefined { + return cursor; +} + +export function setCursor(val: number | undefined): void { + cursor = val; +} + +export function resetPoller(): void { + stopPolling(); + cursor = undefined; +} diff --git a/src/indexer/registry.js b/src/indexer/registry.js new file mode 100644 index 0000000..50ed99b --- /dev/null +++ b/src/indexer/registry.js @@ -0,0 +1,15 @@ +const handlers = new Map(); +export function registerEventHandler(topic, handler) { + handlers.set(topic, handler); +} +export async function dispatch(event) { + const handler = handlers.get(event.topic); + if (!handler) { + console.log(`No handler registered for topic "${event.topic}", skipping.`); + return; + } + await handler(event); +} +export function clearHandlers() { + handlers.clear(); +} diff --git a/src/indexer/registry.ts b/src/indexer/registry.ts new file mode 100644 index 0000000..dbcac03 --- /dev/null +++ b/src/indexer/registry.ts @@ -0,0 +1,22 @@ +import { DecodedEvent } from './types.js'; + +export type EventHandler = (event: DecodedEvent) => Promise | void; + +const handlers = new Map(); + +export function registerEventHandler(topic: string, handler: EventHandler): void { + handlers.set(topic, handler); +} + +export async function dispatch(event: DecodedEvent): Promise { + const handler = handlers.get(event.topic); + if (!handler) { + console.log(`No handler registered for topic "${event.topic}", skipping.`); + return; + } + await handler(event); +} + +export function clearHandlers(): void { + handlers.clear(); +} diff --git a/src/indexer/run.js b/src/indexer/run.js new file mode 100644 index 0000000..674a4ec --- /dev/null +++ b/src/indexer/run.js @@ -0,0 +1,13 @@ +import { startPolling, stopPolling } from './poller.js'; +process.on('SIGINT', () => { + console.log('Received SIGINT, shutting down indexer...'); + stopPolling(); +}); +process.on('SIGTERM', () => { + console.log('Received SIGTERM, shutting down indexer...'); + stopPolling(); +}); +startPolling().catch((error) => { + console.error('Fatal error starting Soroban indexer:', error); + process.exit(1); +}); diff --git a/src/indexer/run.ts b/src/indexer/run.ts new file mode 100644 index 0000000..c2d12d8 --- /dev/null +++ b/src/indexer/run.ts @@ -0,0 +1,16 @@ +import { startPolling, stopPolling } from './poller.js'; + +process.on('SIGINT', () => { + console.log('Received SIGINT, shutting down indexer...'); + stopPolling(); +}); + +process.on('SIGTERM', () => { + console.log('Received SIGTERM, shutting down indexer...'); + stopPolling(); +}); + +startPolling().catch((error) => { + console.error('Fatal error starting Soroban indexer:', error); + process.exit(1); +}); diff --git a/src/indexer/sorobanClient.js b/src/indexer/sorobanClient.js new file mode 100644 index 0000000..b39bc62 --- /dev/null +++ b/src/indexer/sorobanClient.js @@ -0,0 +1,4 @@ +import { rpc } from '@stellar/stellar-sdk'; +import { environment } from '../config/environment.js'; +export const sorobanServer = new rpc.Server(environment.stellar.rpcUrl); +export default sorobanServer; diff --git a/src/indexer/sorobanClient.ts b/src/indexer/sorobanClient.ts new file mode 100644 index 0000000..ab3cd93 --- /dev/null +++ b/src/indexer/sorobanClient.ts @@ -0,0 +1,5 @@ +import { rpc } from '@stellar/stellar-sdk'; +import { environment } from '../config/environment.js'; + +export const sorobanServer = new rpc.Server(environment.stellar.rpcUrl); +export default sorobanServer; diff --git a/src/indexer/types.js b/src/indexer/types.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/src/indexer/types.js @@ -0,0 +1 @@ +export {}; diff --git a/src/indexer/types.ts b/src/indexer/types.ts new file mode 100644 index 0000000..4b0e4d4 --- /dev/null +++ b/src/indexer/types.ts @@ -0,0 +1,7 @@ +export interface DecodedEvent { + id: string; + topic: string; + ledger: number; + txHash: string; + data: any; +} diff --git a/src/middlewares/auth.middleware.js b/src/middlewares/auth.middleware.js new file mode 100644 index 0000000..3684a7e --- /dev/null +++ b/src/middlewares/auth.middleware.js @@ -0,0 +1,129 @@ +import jwt from 'jsonwebtoken'; +import prisma from '../config/prisma.js'; +import { environment } from '../config/environment.js'; +import { authenticateApiKey } from '../services/api-key.services.js'; +import { isApiKeyToken } from '../utils/api-key.utils.js'; +const extractBearerToken = (req) => { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return null; + } + const token = authHeader.slice('Bearer '.length).trim(); + return token || null; +}; +const authenticateRefreshToken = async (token) => { + const session = await prisma.refreshToken.findUnique({ + where: { token }, + include: { merchant: true }, + }); + if (!session || session.expiresAt.getTime() < Date.now()) { + return null; + } + return session.merchant; +}; +const authenticateJwt = async (token) => { + try { + const payload = jwt.verify(token, environment.jwtSecret); + if (!payload.sub) { + return null; + } + return prisma.merchant.findUnique({ where: { id: payload.sub } }); + } + catch { + return null; + } +}; +const resolveMerchantFromToken = async (token) => { + if (isApiKeyToken(token)) { + return authenticateApiKey(token); + } + if (token.split('.').length === 3) { + return authenticateJwt(token); + } + return authenticateRefreshToken(token); +}; +/** + * Authenticates API key bearer tokens, updates lastUsedAt, and attaches the merchant. + */ +export const apiKeyAuth = async (req, res, next) => { + try { + const token = extractBearerToken(req); + if (!token) { + res.status(401).json({ error: 'Authentication required' }); + return; + } + if (!isApiKeyToken(token)) { + res.status(401).json({ error: 'Invalid or expired token' }); + return; + } + const merchant = await authenticateApiKey(token); + if (!merchant) { + res.status(401).json({ error: 'Invalid or expired token' }); + return; + } + req.merchant = merchant; + next(); + } + catch { + res.status(500).json({ error: 'Internal Server Error' }); + } +}; +/** + * Authenticates a merchant using refresh tokens or JWT access tokens only. + * API keys are rejected to prevent key-management operations via API keys. + */ +export const authenticateSessionOnly = async (req, res, next) => { + try { + const token = extractBearerToken(req); + if (!token) { + res.status(401).json({ error: 'Authentication required' }); + return; + } + if (isApiKeyToken(token)) { + res.status(401).json({ error: 'Invalid or expired token' }); + return; + } + const merchant = token.split('.').length === 3 + ? await authenticateJwt(token) + : await authenticateRefreshToken(token); + if (!merchant) { + res.status(401).json({ error: 'Invalid or expired token' }); + return; + } + req.merchant = merchant; + next(); + } + catch { + res.status(500).json({ error: 'Internal Server Error' }); + } +}; +/** + * Authenticates a merchant from a bearer token. + * + * Accepts JWT access tokens (signed with `JWT_SECRET`), refresh session tokens, + * or API keys. The resolved Merchant is attached to `req.merchant` on success. + * + * Responds with 401 when the `Authorization: Bearer ` header is missing + * or malformed (`Authentication required`), or when the token is invalid, + * expired, or references a merchant that no longer exists + * (`Invalid or expired token`). + */ +export const authenticateMerchant = async (req, res, next) => { + try { + const token = extractBearerToken(req); + if (!token) { + res.status(401).json({ error: 'Authentication required' }); + return; + } + const merchant = await resolveMerchantFromToken(token); + if (!merchant) { + res.status(401).json({ error: 'Invalid or expired token' }); + return; + } + req.merchant = merchant; + next(); + } + catch { + res.status(500).json({ error: 'Internal Server Error' }); + } +}; diff --git a/src/routes/auth.routes.js b/src/routes/auth.routes.js new file mode 100644 index 0000000..ff579f3 --- /dev/null +++ b/src/routes/auth.routes.js @@ -0,0 +1,9 @@ +import { Router } from 'express'; +import { createNonceController, verifySignatureController, verifyEmailController, resendOtpController, } from '../controllers/auth.controllers.js'; +import { authenticateMerchant } from '../middlewares/auth.middleware.js'; +const router = Router(); +router.post('/nonce', createNonceController); +router.post('/verify', verifySignatureController); +router.post('/verify-email', authenticateMerchant, verifyEmailController); +router.post('/resend-otp', authenticateMerchant, resendOtpController); +export default router; diff --git a/src/routes/index.js b/src/routes/index.js new file mode 100644 index 0000000..2209819 --- /dev/null +++ b/src/routes/index.js @@ -0,0 +1,11 @@ +import merchantRoutes from './merchant.routes.js'; +import authRoutes from './auth.routes.js'; +import invoiceRoutes from './invoice.routes.js'; +import payRoutes from './pay.routes.js'; +import { Router } from 'express'; +const router = Router(); +router.use('/merchants', merchantRoutes); +router.use('/auth', authRoutes); +router.use('/invoices', invoiceRoutes); +router.use('/pay', payRoutes); +export default router; diff --git a/src/routes/invoice.routes.js b/src/routes/invoice.routes.js new file mode 100644 index 0000000..1bd704a --- /dev/null +++ b/src/routes/invoice.routes.js @@ -0,0 +1,12 @@ +import { Router } from 'express'; +import { createInvoiceController, getInvoiceController, getInvoicePdfController, listInvoicesController, sendInvoiceController, voidInvoiceController, } from '../controllers/invoice.controllers.js'; +import { authenticateMerchant } from '../middlewares/auth.middleware.js'; +const router = Router(); +router.use(authenticateMerchant); +router.post('/', createInvoiceController); +router.get('/', listInvoicesController); +router.get('/:id', getInvoiceController); +router.get('/:id/pdf', getInvoicePdfController); +router.post('/:id/send', sendInvoiceController); +router.patch('/:id/void', voidInvoiceController); +export default router; diff --git a/src/routes/merchant.routes.js b/src/routes/merchant.routes.js new file mode 100644 index 0000000..721bba7 --- /dev/null +++ b/src/routes/merchant.routes.js @@ -0,0 +1,16 @@ +import { Router } from 'express'; +import { createMerchantController, getMerchantController, listMerchantsController, registerMerchantController, getMyProfileController, updateMyProfileController, generateSigningKeyController, } from '../controllers/merchant.controllers.js'; +import { createApiKeyController, listApiKeysController, revokeApiKeyController, } from '../controllers/api-key.controllers.js'; +import { authenticateMerchant, authenticateSessionOnly } from '../middlewares/auth.middleware.js'; +const router = Router(); +router.post('/register', authenticateMerchant, registerMerchantController); +router.get('/me', authenticateMerchant, getMyProfileController); +router.patch('/me', authenticateMerchant, updateMyProfileController); +router.post('/signing-key', authenticateSessionOnly, generateSigningKeyController); +router.post('/api-keys', authenticateSessionOnly, createApiKeyController); +router.get('/api-keys', authenticateSessionOnly, listApiKeysController); +router.delete('/api-keys/:id', authenticateSessionOnly, revokeApiKeyController); +router.post('/', createMerchantController); +router.get('/:id', getMerchantController); +router.get('/', listMerchantsController); +export default router; diff --git a/src/routes/pay.routes.js b/src/routes/pay.routes.js new file mode 100644 index 0000000..54f2861 --- /dev/null +++ b/src/routes/pay.routes.js @@ -0,0 +1,7 @@ +import { Router } from 'express'; +import { resolveInvoiceController, confirmPaymentController, getInvoicePdfController, } from '../controllers/pay.controllers.js'; +const router = Router(); +router.get('/:slug', resolveInvoiceController); +router.get('/:slug/pdf', getInvoicePdfController); +router.post('/:slug/confirm', confirmPaymentController); +export default router; diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..b08c2d4 --- /dev/null +++ b/src/server.js @@ -0,0 +1,15 @@ +import app from './app.js'; +import { environment } from './config/environment.js'; +const startServer = async () => { + try { + // Start Express server + app.listen(environment.port, () => { + console.log(`Server running on port ${environment.port} in ${environment.nodeEnv} mode`); + }); + } + catch (error) { + console.error('Error starting server:', error); + process.exit(1); + } +}; +startServer(); diff --git a/src/services/api-key.services.js b/src/services/api-key.services.js new file mode 100644 index 0000000..b02ca85 --- /dev/null +++ b/src/services/api-key.services.js @@ -0,0 +1,89 @@ +import prisma from '../config/prisma.js'; +import { AppError } from '../utils/errors.js'; +import { generateApiKeyMaterial, hashApiKey, MAX_ACTIVE_API_KEYS } from '../utils/api-key.utils.js'; +const toApiKeySummary = (apiKey) => ({ + id: apiKey.id, + prefix: apiKey.prefix ?? '', + label: apiKey.name, + lastUsedAt: apiKey.lastUsedAt, + createdAt: apiKey.createdAt, +}); +const activeApiKeyWhere = (merchantId) => ({ + merchantId, + revokedAt: null, + OR: [{ expiresAt: null }, { expiresAt: { gt: new Date() } }], +}); +export const createApiKey = async (merchantId, label) => { + const { rawKey, prefix, keyHash } = generateApiKeyMaterial(); + const normalizedLabel = label?.trim() || null; + const apiKey = await prisma.$transaction(async (tx) => { + const activeKeys = await tx.apiKey.count({ + where: activeApiKeyWhere(merchantId), + }); + if (activeKeys >= MAX_ACTIVE_API_KEYS) { + throw new AppError(400, `Maximum of ${MAX_ACTIVE_API_KEYS} active API keys allowed`); + } + return tx.apiKey.create({ + data: { + merchantId, + keyHash, + prefix, + name: normalizedLabel, + }, + }); + }); + return { + ...toApiKeySummary(apiKey), + key: rawKey, + }; +}; +export const listApiKeys = async (merchantId) => { + const apiKeys = await prisma.apiKey.findMany({ + where: { + merchantId, + revokedAt: null, + }, + orderBy: { createdAt: 'desc' }, + select: { + id: true, + prefix: true, + name: true, + lastUsedAt: true, + createdAt: true, + }, + }); + return apiKeys.map(toApiKeySummary); +}; +export const revokeApiKey = async (merchantId, keyId) => { + const apiKey = await prisma.apiKey.findFirst({ + where: { id: keyId, merchantId }, + }); + if (!apiKey) { + throw new AppError(404, 'API key not found'); + } + if (apiKey.revokedAt) { + throw new AppError(400, 'API key already revoked'); + } + await prisma.apiKey.update({ + where: { id: keyId }, + data: { revokedAt: new Date() }, + }); +}; +export const authenticateApiKey = async (rawKey) => { + const keyHash = hashApiKey(rawKey); + const apiKey = await prisma.apiKey.findUnique({ + where: { keyHash }, + include: { merchant: true }, + }); + if (!apiKey || apiKey.revokedAt) { + return null; + } + if (apiKey.expiresAt && apiKey.expiresAt.getTime() < Date.now()) { + return null; + } + await prisma.apiKey.update({ + where: { id: apiKey.id }, + data: { lastUsedAt: new Date() }, + }); + return apiKey.merchant; +}; diff --git a/src/services/auth.services.js b/src/services/auth.services.js new file mode 100644 index 0000000..f45a531 --- /dev/null +++ b/src/services/auth.services.js @@ -0,0 +1,100 @@ +import crypto from 'node:crypto'; +import jwt from 'jsonwebtoken'; +import { Keypair } from '@stellar/stellar-sdk'; +import prisma from '../config/prisma.js'; +import { environment } from '../config/environment.js'; +const NONCE_EXPIRY_MS = 5 * 60 * 1000; +const REFRESH_TOKEN_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000; +export function buildChallengeMessage(address, nonce, createdAt) { + return [ + 'Shade Authentication', + `Address: ${address}`, + `Nonce: ${nonce}`, + `Timestamp: ${createdAt.toISOString()}`, + ].join('\n'); +} +export async function createNonce(address) { + const nonce = crypto.randomUUID(); + const createdAt = new Date(); + const expiresAt = new Date(createdAt.getTime() + NONCE_EXPIRY_MS); + const message = buildChallengeMessage(address, nonce, createdAt); + const authNonce = await prisma.authNonce.create({ + data: { address, nonce, message, expiresAt }, + }); + return { nonce: authNonce.nonce, message: authNonce.message, expiresAt: authNonce.expiresAt }; +} +export async function verifySignature(address, nonce, rawSignature) { + const authNonce = await prisma.authNonce.findUnique({ where: { nonce } }); + if (!authNonce) { + return { valid: false, reason: 'Nonce not found' }; + } + if (authNonce.address !== address) { + return { valid: false, reason: 'Address mismatch' }; + } + if (authNonce.usedAt) { + return { valid: false, reason: 'Nonce already used' }; + } + if (new Date() > authNonce.expiresAt) { + return { valid: false, reason: 'Nonce expired' }; + } + const message = buildChallengeMessage(address, authNonce.nonce, authNonce.createdAt); + const messageBytes = Buffer.from(message, 'utf-8'); + const signatureBytes = Buffer.from(rawSignature, 'hex'); + let isValid; + try { + const keypair = Keypair.fromPublicKey(address); + isValid = keypair.verify(messageBytes, signatureBytes); + } + catch { + return { valid: false, reason: 'Invalid address or signature format' }; + } + if (!isValid) { + return { valid: false, reason: 'Signature verification failed' }; + } + await prisma.authNonce.update({ + where: { id: authNonce.id }, + data: { usedAt: new Date() }, + }); + return { valid: true, reason: null }; +} +export async function upsertMerchant(address) { + const existing = await prisma.merchant.findFirst({ where: { address } }); + if (existing) { + return existing; + } + const merchantId = crypto.randomInt(100_000, 999_999); + const merchant = await prisma.merchant.create({ + data: { merchantId, address }, + }); + return merchant; +} +export function issueAccessToken(merchantId, address) { + return jwt.sign({ sub: merchantId, address }, environment.jwtSecret, { expiresIn: '15m' }); +} +export async function issueRefreshToken(merchantId) { + const token = crypto.randomUUID(); + const expiresAt = new Date(Date.now() + REFRESH_TOKEN_EXPIRY_MS); + await prisma.refreshToken.create({ + data: { merchantId, token, expiresAt }, + }); + return token; +} +export async function authenticateWallet(address, nonce, signature) { + const verification = await verifySignature(address, nonce, signature); + if (!verification.valid) { + return { success: false, reason: verification.reason }; + } + const merchant = await upsertMerchant(address); + const accessToken = issueAccessToken(merchant.id, merchant.address); + const refreshToken = await issueRefreshToken(merchant.id); + return { + success: true, + accessToken, + refreshToken, + merchant: { + id: merchant.id, + address: merchant.address, + isRegistered: merchant.registered, + }, + }; +} diff --git a/src/services/email.service.js b/src/services/email.service.js new file mode 100644 index 0000000..977b3c2 --- /dev/null +++ b/src/services/email.service.js @@ -0,0 +1,112 @@ +import nodemailer from 'nodemailer'; +import { Resend } from 'resend'; +import { environment } from '../config/environment.js'; +import { generateInvoicePdf } from './invoice-pdf.services.js'; +const escapeHtml = (value) => value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +const buildOtpEmailContent = (firstName, code) => { + const safeFirstName = escapeHtml(firstName); + const subject = 'Verify your Shade email'; + const html = ` +

Hi ${safeFirstName},

+

Your email verification code is:

+

${code}

+

This code expires in 10 minutes.

+ `.trim(); + const text = `Hi ${firstName},\n\nYour verification code is: ${code}\n\nThis code expires in 10 minutes.`; + return { subject, html, text }; +}; +const sendViaResend = async (to, subject, html, attachments) => { + const resend = new Resend(environment.email.resendApiKey); + const { error } = await resend.emails.send({ + from: environment.email.from, + to, + subject, + html, + attachments: attachments?.map(({ filename, content }) => ({ filename, content })), + }); + if (error) { + throw new Error(`Failed to send email via Resend: ${error.message}`); + } +}; +const sendViaSmtp = async (to, subject, html, text, attachments) => { + const transporter = nodemailer.createTransport({ + host: environment.email.smtp.host, + port: environment.email.smtp.port, + secure: environment.email.smtp.secure, + auth: { + user: environment.email.smtp.user, + pass: environment.email.smtp.pass, + }, + }); + await transporter.sendMail({ + from: environment.email.from, + to, + subject, + html, + text, + attachments, + }); +}; +/** + * Delivers a one-time verification code to the merchant's email address. + */ +export const sendOtp = async (to, code, firstName) => { + const { subject, html, text } = buildOtpEmailContent(firstName, code); + switch (environment.email.provider) { + case 'resend': + await sendViaResend(to, subject, html); + return; + case 'smtp': + await sendViaSmtp(to, subject, html, text); + return; + case 'console': + default: + console.log(`[OTP] Verification code ${code} sent to ${to} for ${firstName}`); + } +}; +const buildInvoiceEmailContent = (invoice, merchant) => { + const merchantName = escapeHtml(merchant.businessName || 'Your merchant'); + const description = escapeHtml(invoice.description); + const subject = `Invoice from ${merchant.businessName || 'Shade'}: ${invoice.description}`; + const html = ` +

Hi,

+

${merchantName} has sent you an invoice for ${description}.

+

Amount: ${invoice.amount.toString()} ${escapeHtml(invoice.token)}

+

Status: ${invoice.status}

+

Your invoice is attached as a PDF.

+ `.trim(); + const text = `Hi,\n\n${merchant.businessName || 'Your merchant'} has sent you an invoice for ${invoice.description}.\n\nAmount: ${invoice.amount.toString()} ${invoice.token}\nStatus: ${invoice.status}\n\nYour invoice is attached as a PDF.`; + return { subject, html, text }; +}; +/** + * Emails the invoice to `invoice.email` with a freshly generated PDF attached. + * No-ops (does not throw) when the invoice has no email on file — callers + * that need to surface that as a user-facing error (e.g. the /send route) + * should check `invoice.email` before calling this. + */ +export const sendInvoiceEmail = async (invoice, merchant) => { + if (!invoice.email) { + return; + } + const pdf = await generateInvoicePdf(invoice, merchant); + const { subject, html, text } = buildInvoiceEmailContent(invoice, merchant); + const attachments = [ + { filename: `invoice-${invoice.paymentSlug}.pdf`, content: pdf }, + ]; + switch (environment.email.provider) { + case 'resend': + await sendViaResend(invoice.email, subject, html, attachments); + return; + case 'smtp': + await sendViaSmtp(invoice.email, subject, html, text, attachments); + return; + case 'console': + default: + console.log(`[Invoice email] Invoice ${invoice.paymentSlug} (${pdf.length} byte PDF) sent`); + } +}; diff --git a/src/services/index.js b/src/services/index.js new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/src/services/index.js @@ -0,0 +1 @@ +export {}; diff --git a/src/services/invoice-pdf.services.js b/src/services/invoice-pdf.services.js new file mode 100644 index 0000000..ac3357c --- /dev/null +++ b/src/services/invoice-pdf.services.js @@ -0,0 +1,95 @@ +import PDFDocument from 'pdfkit'; +const FIXED_FIAT = 'FIXED_FIAT'; +// merchant.logo is a free-form string (set via the merchant profile API). Only a +// data: URI can be embedded without giving this "pure" renderer a network +// dependency, so a plain image URL is intentionally skipped rather than fetched. +const DATA_URI_IMAGE = /^data:image\/(png|jpe?g);base64,([a-z0-9+/=]+)$/i; +const decodeLogo = (logo) => { + if (!logo) + return null; + const match = DATA_URI_IMAGE.exec(logo.trim()); + if (!match) + return null; + try { + return Buffer.from(match[2], 'base64'); + } + catch { + return null; + } +}; +const formatDate = (date) => { + if (!date) + return '-'; + return date + .toISOString() + .replace('T', ' ') + .replace(/\.\d+Z$/, ' UTC'); +}; +const formatFiatAmount = (fiatAmount, fiatDecimals, fiatCurrency) => { + const decimals = Math.max(fiatDecimals, 0); + const divisor = 10n ** BigInt(decimals); + const whole = fiatAmount / divisor; + const fraction = fiatAmount % divisor; + if (decimals === 0) { + return `${whole.toString()} ${fiatCurrency}`; + } + const fractionStr = fraction.toString().padStart(decimals, '0'); + return `${whole.toString()}.${fractionStr} ${fiatCurrency}`; +}; +const drawField = (doc, label, value) => { + doc.font('Helvetica-Bold').fontSize(10).text(label, { continued: true }); + doc.font('Helvetica').fontSize(10).text(` ${value}`); + doc.moveDown(0.5); +}; +/** + * Renders an invoice + merchant pair to a PDF buffer. Pure and side-effect + * free: no database access, no filesystem or network writes. Callers are + * responsible for fetching the records; this only formats what it's given. + */ +export const generateInvoicePdf = (invoice, merchant) => { + return new Promise((resolve, reject) => { + const doc = new PDFDocument({ size: 'A4', margin: 50 }); + const chunks = []; + doc.on('data', chunk => chunks.push(chunk)); + doc.on('end', () => resolve(Buffer.concat(chunks))); + doc.on('error', reject); + const logoBuffer = decodeLogo(merchant.logo); + if (logoBuffer) { + try { + doc.image(logoBuffer, { fit: [80, 80] }); + doc.moveDown(); + } + catch (err) { + // Corrupt/undecodable image data — skip it rather than fail the render, + // but log so real failures (not just bad merchant uploads) stay visible. + console.error(`Failed to embed invoice logo for merchant ${merchant.id}`, err); + } + } + doc + .font('Helvetica-Bold') + .fontSize(18) + .text(merchant.businessName || 'Invoice'); + doc.moveDown(); + doc.font('Helvetica-Bold').fontSize(14).text('Invoice'); + doc.moveDown(0.5); + doc.font('Helvetica').fontSize(11).text(invoice.description); + doc.moveDown(); + drawField(doc, 'Amount:', `${invoice.amount.toString()} ${invoice.token}`); + if (invoice.pricingMode === FIXED_FIAT && + invoice.fiatAmount !== null && + invoice.fiatCurrency !== null && + invoice.fiatDecimals !== null) { + drawField(doc, 'Fiat amount:', formatFiatAmount(invoice.fiatAmount, invoice.fiatDecimals, invoice.fiatCurrency)); + } + drawField(doc, 'Status:', invoice.status); + drawField(doc, 'Payment link:', invoice.paymentSlug); + drawField(doc, 'Created:', formatDate(invoice.createdAt)); + if (invoice.datePaid) { + drawField(doc, 'Paid:', formatDate(invoice.datePaid)); + } + if (invoice.payer) { + drawField(doc, 'Payer address:', invoice.payer); + } + doc.end(); + }); +}; diff --git a/src/services/invoice.services.js b/src/services/invoice.services.js new file mode 100644 index 0000000..9187f92 --- /dev/null +++ b/src/services/invoice.services.js @@ -0,0 +1,143 @@ +import prisma from '../config/prisma.js'; +import { AppError } from '../utils/errors.js'; +import { generatePaymentSlug } from '../utils/slug.js'; +import { parseAmount, } from '../utils/invoice.validation.js'; +const SLUG_MAX_RETRIES = 5; +// String constants matching the Prisma `Status` enum. Defined locally so this +// module never imports a runtime value from `@prisma/client` (the generated +// client is mocked in tests and not generated in CI). +const InvoiceStatus = { + DRAFT: 'DRAFT', + PENDING: 'PENDING', + PAID: 'PAID', + CANCELLED: 'CANCELLED', +}; +/** + * Public-facing view of an invoice. `amount` is serialized to a string because + * `BigInt` is not JSON-serializable. + */ +export const sanitizeInvoice = (invoice) => ({ + id: invoice.id, + paymentSlug: invoice.paymentSlug, + description: invoice.description, + amount: invoice.amount.toString(), + token: invoice.token, + status: invoice.status, + merchantId: invoice.merchantId, + email: invoice.email, + expiresAt: invoice.expiresAt, + datePaid: invoice.datePaid, + createdAt: invoice.createdAt, + updatedAt: invoice.updatedAt, +}); +const isUniqueSlugError = (error) => { + if (typeof error !== 'object' || error === null) + return false; + const { code, meta } = error; + return code === 'P2002' && Array.isArray(meta?.target) && meta.target.includes('paymentSlug'); +}; +export const createInvoice = async (merchantId, data) => { + const amount = parseAmount(data.amount); + if (amount === null) { + throw new AppError(400, 'amount must be a positive integer'); + } + const status = data.isDraft ? InvoiceStatus.DRAFT : InvoiceStatus.PENDING; + const expiresAt = data.expiresAt ? new Date(data.expiresAt) : null; + for (let attempt = 0; attempt < SLUG_MAX_RETRIES; attempt++) { + try { + const invoice = await prisma.invoice.create({ + data: { + merchantId, + description: data.description.trim(), + amount, + token: data.token.trim(), + email: data.payerEmail?.trim() ?? null, + expiresAt, + status, + paymentSlug: generatePaymentSlug(), + }, + }); + return sanitizeInvoice(invoice); + } + catch (error) { + if (isUniqueSlugError(error) && attempt < SLUG_MAX_RETRIES - 1) { + continue; + } + throw error; + } + } + throw new AppError(500, 'Failed to generate a unique payment slug'); +}; +export const listInvoices = async (merchantId, filters, pagination) => { + const where = { merchantId }; + if (filters.status) { + where.status = filters.status; + } + if (filters.token) { + where.token = filters.token; + } + if (filters.startDate || filters.endDate) { + where.createdAt = {}; + if (filters.startDate) + where.createdAt.gte = filters.startDate; + if (filters.endDate) + where.createdAt.lte = filters.endDate; + } + const [invoices, total] = await Promise.all([ + prisma.invoice.findMany({ + where, + take: pagination.limit, + skip: pagination.offset, + orderBy: { createdAt: 'desc' }, + }), + prisma.invoice.count({ where }), + ]); + return { + data: invoices.map(sanitizeInvoice), + pagination: { + limit: pagination.limit, + offset: pagination.offset, + total, + }, + }; +}; +export const getInvoice = async (merchantId, id) => { + const invoice = await prisma.invoice.findFirst({ + where: { id, merchantId }, + }); + if (!invoice) { + throw new AppError(404, 'Invoice not found'); + } + return sanitizeInvoice(invoice); +}; +/** + * Fetches the raw invoice + merchant records, scoped to the owning merchant, + * for the PDF/email flows that need fields beyond the sanitized public view + * (payer address, fiat breakdown, merchant logo). + */ +export const getInvoiceWithMerchant = async (merchantId, id) => { + const invoice = await prisma.invoice.findFirst({ + where: { id, merchantId }, + include: { merchant: true }, + }); + if (!invoice) { + throw new AppError(404, 'Invoice not found'); + } + return invoice; +}; +export const voidInvoice = async (merchantId, id) => { + const invoice = await prisma.invoice.findFirst({ + where: { id, merchantId }, + }); + if (!invoice) { + throw new AppError(404, 'Invoice not found'); + } + if (invoice.status !== InvoiceStatus.PENDING) { + throw new AppError(400, 'Only pending invoices can be voided'); + } + const updated = await prisma.invoice.update({ + where: { id: invoice.id }, + data: { status: InvoiceStatus.CANCELLED }, + }); + return sanitizeInvoice(updated); +}; diff --git a/src/services/merchant.services.js b/src/services/merchant.services.js new file mode 100644 index 0000000..64693a4 --- /dev/null +++ b/src/services/merchant.services.js @@ -0,0 +1,191 @@ +import prisma from '../config/prisma.js'; +import { AppError } from '../utils/errors.js'; +import { generateOtp, hashOtp } from './otp.services.js'; +import { sendOtp } from './email.service.js'; +import { Keypair } from '@stellar/stellar-sdk'; +const OTP_EXPIRY_MS = 10 * 60 * 1000; +/** + * Returns a public-facing view of a merchant. Built as an allow-list so that + * any sensitive fields added to the model later are never exposed by default. + */ +export const sanitizeMerchant = (merchant) => ({ + id: merchant.id, + merchantId: merchant.merchantId, + email: merchant.email, + address: merchant.address, + account: merchant.account, + merchantKey: merchant.merchantKey, + firstName: merchant.firstName, + lastName: merchant.lastName, + businessName: merchant.businessName, + category: merchant.category, + description: merchant.description, + logo: merchant.logo, + webhook: merchant.webhook, + active: merchant.active, + verified: merchant.verified, + emailVerified: merchant.emailVerified, + registered: merchant.registered, + createdAt: merchant.createdAt, + updatedAt: merchant.updatedAt, +}); +export const createMerchant = async (merchantData) => { + try { + const merchant = await prisma.merchant.create({ + data: merchantData, + }); + return merchant; + } + catch (error) { + throw error; + } +}; +export const getMerchant = async (merchantId) => { + try { + const merchant = await prisma.merchant.findUnique({ + where: { + merchantId: merchantId, + }, + }); + return merchant; + } + catch (error) { + throw error; + } +}; +export const listMerchants = async (limit, offset) => { + try { + const merchants = await prisma.merchant.findMany({ + take: limit, + skip: offset, + }); + return merchants; + } + catch (error) { + throw error; + } +}; +/** + * Completes a merchant's profile after wallet authentication. + * + * Enforces that the email is unique across merchants and that the profile has + * not already been completed, persists the profile data, resets email + * verification, and triggers an OTP email. + */ +export const registerMerchant = async (merchantId, data) => { + const merchant = await prisma.merchant.findUnique({ + where: { id: merchantId }, + }); + if (!merchant) { + throw new AppError(404, 'Merchant not found'); + } + if (merchant.registered) { + throw new AppError(409, 'Profile already set up'); + } + const normalizedEmail = data.email.trim().toLowerCase(); + const existingEmail = await prisma.merchant.findFirst({ + where: { + email: normalizedEmail, + NOT: { id: merchantId }, + }, + }); + if (existingEmail) { + throw new AppError(409, 'Email already registered'); + } + const code = generateOtp(); + const emailOtp = await hashOtp(code); + const emailOtpExpiresAt = new Date(Date.now() + OTP_EXPIRY_MS); + const updatedMerchant = await prisma.merchant.update({ + where: { id: merchantId }, + data: { + firstName: data.firstName.trim(), + lastName: data.lastName.trim(), + email: normalizedEmail, + businessName: data.businessName.trim(), + category: data.category.trim(), + description: data.description.trim(), + logo: data.logo?.trim() ?? null, + emailVerified: false, + registered: true, + emailOtp, + emailOtpExpiresAt, + }, + }); + try { + await sendOtp(normalizedEmail, code, data.firstName.trim()); + } + catch (err) { + console.error('Failed to send OTP email after registration', err); + } + return sanitizeMerchant(updatedMerchant); +}; +/** + * Returns the authenticated merchant's own profile. + */ +export const getMyProfile = async (id) => { + const merchant = await prisma.merchant.findUnique({ where: { id } }); + if (!merchant) { + throw new AppError(404, 'Merchant not found'); + } + return sanitizeMerchant(merchant); +}; +/** + * Generates a fresh Ed25519 signing keypair for the merchant. + * + * Persists ONLY the hex-encoded 32-byte public key to `Merchant.merchantKey`, + * overwriting any previous value (unconditional generate-and-replace). Returns + * both halves; the hex-encoded 32-byte private key is returned exactly once and + * is never written to the database or logged. + * + * Uploading the public key on-chain (`set_merchant_key`) and signing invoices + * with the private key are done client/SDK-side and are out of scope here. + */ +export const generateMerchantSigningKey = async (id) => { + const merchant = await prisma.merchant.findUnique({ where: { id } }); + if (!merchant) { + throw new AppError(404, 'Merchant not found'); + } + const keypair = Keypair.random(); + const publicKey = Buffer.from(keypair.rawPublicKey()).toString('hex'); + const privateKey = Buffer.from(keypair.rawSecretKey()).toString('hex'); + // Optimistic concurrency: only replace the key we just read. If a concurrent + // rotation already changed it, no row matches and we reject rather than return + // a private key whose public half is no longer the one persisted. + const { count } = await prisma.merchant.updateMany({ + where: { id, merchantKey: merchant.merchantKey }, + data: { merchantKey: publicKey }, + }); + if (count !== 1) { + throw new AppError(409, 'Signing key was changed concurrently; please retry'); + } + // Audit only — never include the private key here. + console.info(`[merchant] signing key ${merchant.merchantKey ? 'rotated' : 'created'} for merchant ${id}`); + return { publicKey, privateKey }; +}; +/** + * Partially updates the authenticated merchant's editable profile fields. + * + * Only fields present in `data` are written. Strings are trimmed; an empty + * `logo`/`webhook` is normalized to null so the merchant can clear them. + * Non-editable fields are never read here, so they cannot be changed. + */ +export const updateMyProfile = async (id, data) => { + const updateData = {}; + const textFields = ['firstName', 'lastName', 'businessName', 'category', 'description']; + for (const field of textFields) { + const value = data[field]; + if (value !== undefined) { + updateData[field] = value.trim(); + } + } + if (data.logo !== undefined) { + const logo = typeof data.logo === 'string' ? data.logo.trim() : data.logo; + updateData.logo = logo ? logo : null; + } + if (data.webhook !== undefined) { + const webhook = typeof data.webhook === 'string' ? data.webhook.trim() : data.webhook; + updateData.webhook = webhook ? webhook : null; + } + const updated = await prisma.merchant.update({ where: { id }, data: updateData }); + return sanitizeMerchant(updated); +}; diff --git a/src/services/otp.services.js b/src/services/otp.services.js new file mode 100644 index 0000000..3ae9e2d --- /dev/null +++ b/src/services/otp.services.js @@ -0,0 +1,85 @@ +import { randomInt } from 'node:crypto'; +import bcrypt from 'bcrypt'; +import prisma from '../config/prisma.js'; +import { AppError } from '../utils/errors.js'; +import { sendOtp } from './email.service.js'; +const OTP_LENGTH = 6; +const OTP_EXPIRY_MS = 10 * 60 * 1000; +const OTP_RESEND_COOLDOWN_MS = 60 * 1000; +const BCRYPT_ROUNDS = 10; +export const generateOtp = () => { + const min = 10 ** (OTP_LENGTH - 1); + const max = 10 ** OTP_LENGTH - 1; + return randomInt(min, max + 1).toString(); +}; +export const hashOtp = async (code) => bcrypt.hash(code, BCRYPT_ROUNDS); +export const verifyOtpHash = async (code, hash) => bcrypt.compare(code, hash); +const getLastOtpSentAt = (expiresAt) => new Date(expiresAt.getTime() - OTP_EXPIRY_MS); +/** + * Generates a 6-digit OTP, stores its bcrypt hash with a 10-minute expiry, + * and sends the code to the merchant's email. + */ +export const issueEmailOtp = async (merchant) => { + const code = generateOtp(); + const emailOtp = await hashOtp(code); + const emailOtpExpiresAt = new Date(Date.now() + OTP_EXPIRY_MS); + await prisma.merchant.update({ + where: { id: merchant.id }, + data: { emailOtp, emailOtpExpiresAt }, + }); + await sendOtp(merchant.email, code, merchant.firstName?.trim() || 'there'); +}; +/** + * Validates the submitted OTP against the stored hash and marks the email verified. + */ +export const verifyEmailOtp = async (merchantId, code) => { + const merchant = await prisma.merchant.findUnique({ + where: { id: merchantId }, + }); + if (!merchant?.emailOtp || !merchant.emailOtpExpiresAt) { + throw new AppError(400, 'Invalid verification code'); + } + if (merchant.emailOtpExpiresAt.getTime() < Date.now()) { + throw new AppError(400, 'Code expired'); + } + const isValid = await verifyOtpHash(code, merchant.emailOtp); + if (!isValid) { + throw new AppError(400, 'Invalid verification code'); + } + return prisma.merchant.update({ + where: { id: merchantId }, + data: { + emailVerified: true, + emailOtp: null, + emailOtpExpiresAt: null, + }, + }); +}; +/** + * Re-generates and re-sends the email OTP, rate-limited to one request per minute. + */ +export const resendEmailOtp = async (merchantId) => { + const merchant = await prisma.merchant.findUnique({ + where: { id: merchantId }, + }); + if (!merchant) { + throw new AppError(404, 'Merchant not found'); + } + if (!merchant.registered || !merchant.email) { + throw new AppError(400, 'Registration incomplete'); + } + if (merchant.emailVerified) { + throw new AppError(400, 'Email already verified'); + } + if (merchant.emailOtpExpiresAt) { + const lastSentAt = getLastOtpSentAt(merchant.emailOtpExpiresAt); + if (Date.now() - lastSentAt.getTime() < OTP_RESEND_COOLDOWN_MS) { + throw new AppError(429, 'Please wait before requesting a new code'); + } + } + await issueEmailOtp({ + id: merchant.id, + email: merchant.email, + firstName: merchant.firstName, + }); +}; diff --git a/src/services/pay.services.js b/src/services/pay.services.js new file mode 100644 index 0000000..d033d41 --- /dev/null +++ b/src/services/pay.services.js @@ -0,0 +1,93 @@ +import prisma from '../config/prisma.js'; +import { AppError } from '../utils/errors.js'; +const InvoiceStatus = { + DRAFT: 'DRAFT', + PENDING: 'PENDING', + PAID: 'PAID', + CANCELLED: 'CANCELLED', + REFUNDED: 'REFUNDED', +}; +const assertInvoiceVisible = (invoice) => { + if (invoice.status === InvoiceStatus.CANCELLED || + invoice.status === InvoiceStatus.PAID || + invoice.status === InvoiceStatus.REFUNDED) { + throw new AppError(410, 'Invoice is no longer available'); + } + if (invoice.expiresAt && invoice.expiresAt < new Date()) { + throw new AppError(410, 'expired'); + } +}; +export const resolveInvoiceBySlug = async (slug) => { + const invoice = await prisma.invoice.findUnique({ + where: { paymentSlug: slug }, + select: { + paymentSlug: true, + description: true, + amount: true, + token: true, + status: true, + expiresAt: true, + pricingMode: true, + merchant: { + select: { + businessName: true, + }, + }, + }, + }); + if (!invoice) { + throw new AppError(404, 'Invoice not found'); + } + assertInvoiceVisible(invoice); + return { + slug: invoice.paymentSlug, + description: invoice.description, + amount: invoice.amount.toString(), + token: invoice.token, + status: invoice.status, + merchantName: invoice.merchant.businessName, + expiresAt: invoice.expiresAt, + pricingMode: invoice.pricingMode, + }; +}; +/** + * Fetches the full invoice + merchant records for a publicly visible invoice, + * applying the same 404/410 visibility rules as `resolveInvoiceBySlug`. Used + * by the public PDF download route, which needs raw fields (payer, dates, + * fiat breakdown, logo) rather than the trimmed public-facing view. + */ +export const getInvoiceForPdfBySlug = async (slug) => { + const invoice = await prisma.invoice.findUnique({ + where: { paymentSlug: slug }, + include: { merchant: true }, + }); + if (!invoice) { + throw new AppError(404, 'Invoice not found'); + } + assertInvoiceVisible(invoice); + return invoice; +}; +export const confirmPayment = async (slug, payerAddress, txHash) => { + return await prisma.$transaction(async (tx) => { + const invoice = await tx.invoice.findUnique({ + where: { paymentSlug: slug }, + }); + if (!invoice) { + throw new AppError(404, 'Invoice not found'); + } + assertInvoiceVisible(invoice); + const idempotencyKey = `${invoice.id}-${payerAddress}-${txHash || 'none'}`; + const confirmation = await tx.paymentConfirmation.upsert({ + where: { idempotencyKey }, + update: {}, + create: { + invoiceId: invoice.id, + merchantId: invoice.merchantId, + payerAddress, + txHash: txHash || null, + idempotencyKey, + }, + }); + return confirmation; + }); +}; diff --git a/src/services/storage/invoice-pdf.storage.js b/src/services/storage/invoice-pdf.storage.js new file mode 100644 index 0000000..2849575 --- /dev/null +++ b/src/services/storage/invoice-pdf.storage.js @@ -0,0 +1,3 @@ +export const mockInvoicePdfStorage = { + upload: async (key) => ({ url: `mock://invoice-pdfs/${key}` }), +}; diff --git a/src/utils/api-key.utils.js b/src/utils/api-key.utils.js new file mode 100644 index 0000000..2d237ad --- /dev/null +++ b/src/utils/api-key.utils.js @@ -0,0 +1,15 @@ +import crypto from 'node:crypto'; +export const API_KEY_PREFIX = 'sk_live_'; +export const API_KEY_RANDOM_LENGTH = 32; +export const API_KEY_DISPLAY_PREFIX_LENGTH = 8; +export const MAX_ACTIVE_API_KEYS = 10; +const KEY_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; +export const isApiKeyToken = (token) => token.startsWith(API_KEY_PREFIX); +export const hashApiKey = (rawKey) => crypto.createHash('sha256').update(rawKey).digest('hex'); +export const generateApiKeyMaterial = () => { + const randomPart = Array.from({ length: API_KEY_RANDOM_LENGTH }, () => KEY_ALPHABET[crypto.randomInt(0, KEY_ALPHABET.length)]).join(''); + const rawKey = `${API_KEY_PREFIX}${randomPart}`; + const prefix = `${API_KEY_PREFIX}${randomPart.slice(0, API_KEY_DISPLAY_PREFIX_LENGTH)}`; + const keyHash = hashApiKey(rawKey); + return { rawKey, prefix, keyHash }; +}; diff --git a/src/utils/errors.js b/src/utils/errors.js new file mode 100644 index 0000000..c69500e --- /dev/null +++ b/src/utils/errors.js @@ -0,0 +1,9 @@ +export class AppError extends Error { + statusCode; + constructor(statusCode, message) { + super(message); + this.statusCode = statusCode; + this.name = 'AppError'; + Object.setPrototypeOf(this, AppError.prototype); + } +} diff --git a/src/utils/invoice.validation.js b/src/utils/invoice.validation.js new file mode 100644 index 0000000..6c8fb23 --- /dev/null +++ b/src/utils/invoice.validation.js @@ -0,0 +1,121 @@ +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; +const POSITIVE_INTEGER_REGEX = /^\d+$/; +// String constants matching the Prisma `Status` enum. Defined locally so this +// module never imports a runtime value from `@prisma/client` (the generated +// client is mocked in tests and not generated in CI). +const INVOICE_STATUSES = [ + 'DRAFT', + 'PENDING', + 'PAID', + 'CANCELLED', + 'REFUNDED', + 'PARTIALLY_REFUNDED', + 'PARTIALLY_PAID', +]; +export const DEFAULT_LIMIT = 20; +export const MAX_LIMIT = 100; +const isNonEmptyString = (value) => typeof value === 'string' && value.trim().length > 0; +/** + * Parses and validates a positive integer amount supplied as a number or a + * numeric string. Returns the BigInt value, or null when invalid. + */ +export const parseAmount = (value) => { + if (typeof value === 'number') { + if (!Number.isInteger(value) || value <= 0) + return null; + return BigInt(value); + } + if (typeof value === 'string' && POSITIVE_INTEGER_REGEX.test(value.trim())) { + const parsed = BigInt(value.trim()); + return parsed > 0n ? parsed : null; + } + return null; +}; +export const validateCreateInvoice = (body) => { + const errors = {}; + const payload = (body ?? {}); + if (!isNonEmptyString(payload.description)) { + errors.description = 'description is required'; + } + if (parseAmount(payload.amount) === null) { + errors.amount = 'amount must be a positive integer'; + } + if (!isNonEmptyString(payload.token)) { + errors.token = 'token must be a non-empty string'; + } + if (payload.payerEmail !== undefined && + payload.payerEmail !== null && + !(isNonEmptyString(payload.payerEmail) && + EMAIL_REGEX.test(payload.payerEmail.trim()))) { + errors.payerEmail = 'payerEmail must be a valid email'; + } + if (payload.expiresAt !== undefined && payload.expiresAt !== null) { + const date = new Date(payload.expiresAt); + if (Number.isNaN(date.getTime())) { + errors.expiresAt = 'expiresAt must be a valid date'; + } + } + if (payload.isDraft !== undefined && typeof payload.isDraft !== 'boolean') { + errors.isDraft = 'isDraft must be a boolean'; + } + return errors; +}; +/** + * Parses list query parameters into typed filters and pagination, clamping the + * page size to [1, MAX_LIMIT] and defaulting to DEFAULT_LIMIT. + */ +export const parseInvoiceListQuery = (query) => { + const errors = {}; + const filters = {}; + if (query.status !== undefined) { + const status = String(query.status).toUpperCase(); + if (INVOICE_STATUSES.includes(status)) { + filters.status = status; + } + else { + errors.status = `status must be one of ${INVOICE_STATUSES.join(', ')}`; + } + } + if (isNonEmptyString(query.token)) { + filters.token = query.token.trim(); + } + if (query.startDate !== undefined) { + const date = new Date(String(query.startDate)); + if (Number.isNaN(date.getTime())) { + errors.startDate = 'startDate must be a valid date'; + } + else { + filters.startDate = date; + } + } + if (query.endDate !== undefined) { + const date = new Date(String(query.endDate)); + if (Number.isNaN(date.getTime())) { + errors.endDate = 'endDate must be a valid date'; + } + else { + filters.endDate = date; + } + } + let limit = DEFAULT_LIMIT; + if (query.limit !== undefined) { + const parsed = Number(query.limit); + if (!Number.isFinite(parsed) || parsed < 1) { + errors.limit = 'limit must be a positive number'; + } + else { + limit = Math.min(Math.floor(parsed), MAX_LIMIT); + } + } + let offset = 0; + if (query.offset !== undefined) { + const parsed = Number(query.offset); + if (!Number.isFinite(parsed) || parsed < 0) { + errors.offset = 'offset must be a non-negative number'; + } + else { + offset = Math.floor(parsed); + } + } + return { filters, pagination: { limit, offset }, errors }; +}; diff --git a/src/utils/slug.js b/src/utils/slug.js new file mode 100644 index 0000000..200ebdf --- /dev/null +++ b/src/utils/slug.js @@ -0,0 +1,9 @@ +import { randomBytes } from 'crypto'; +/** + * Generates a url-safe, collision-resistant payment slug. + * + * Uses base64url encoding (characters A-Z, a-z, 0-9, `-`, `_`) so the slug can + * be embedded directly in a payment URL without escaping. 12 random bytes yield + * 16 characters and ~96 bits of entropy. + */ +export const generatePaymentSlug = () => randomBytes(12).toString('base64url'); diff --git a/src/utils/validation.js b/src/utils/validation.js new file mode 100644 index 0000000..07f3d92 --- /dev/null +++ b/src/utils/validation.js @@ -0,0 +1,76 @@ +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; +const REQUIRED_FIELDS = [ + 'firstName', + 'lastName', + 'email', + 'businessName', + 'category', + 'description', +]; +const isNonEmptyString = (value) => typeof value === 'string' && value.trim().length > 0; +export const validateRegisterMerchant = (body) => { + const errors = {}; + const payload = (body ?? {}); + for (const field of REQUIRED_FIELDS) { + if (!isNonEmptyString(payload[field])) { + errors[field] = `${field} is required`; + } + } + if (isNonEmptyString(payload.email) && !EMAIL_REGEX.test(payload.email.trim())) { + errors.email = 'A valid email is required'; + } + if (payload.logo !== undefined && typeof payload.logo !== 'string') { + errors.logo = 'logo must be a string'; + } + return errors; +}; +const EDITABLE_MERCHANT_FIELDS = [ + 'firstName', + 'lastName', + 'businessName', + 'category', + 'description', + 'logo', + 'webhook', +]; +const UPDATE_REQUIRED_TEXT_FIELDS = [ + 'firstName', + 'lastName', + 'businessName', + 'category', + 'description', +]; +const isValidHttpsUrl = (value) => { + try { + return new URL(value).protocol === 'https:'; + } + catch { + return false; + } +}; +export const validateUpdateMerchant = (body) => { + const errors = {}; + const payload = (body ?? {}); + const present = EDITABLE_MERCHANT_FIELDS.filter(field => payload[field] !== undefined); + if (present.length === 0) { + errors._empty = 'At least one valid field is required'; + return errors; + } + for (const field of UPDATE_REQUIRED_TEXT_FIELDS) { + if (payload[field] !== undefined && !isNonEmptyString(payload[field])) { + errors[field] = `${field} must be a non-empty string`; + } + } + if (payload.logo !== undefined && payload.logo !== null && typeof payload.logo !== 'string') { + errors.logo = 'logo must be a string or null'; + } + if (payload.webhook !== undefined && payload.webhook !== null) { + if (typeof payload.webhook !== 'string') { + errors.webhook = 'webhook must be a string or null'; + } + else if (payload.webhook.trim().length > 0 && !isValidHttpsUrl(payload.webhook.trim())) { + errors.webhook = 'webhook must be a valid HTTPS URL'; + } + } + return errors; +}; diff --git a/tests/unit/indexer.test.ts b/tests/unit/indexer.test.ts new file mode 100644 index 0000000..d1f8d46 --- /dev/null +++ b/tests/unit/indexer.test.ts @@ -0,0 +1,171 @@ +import { jest } from '@jest/globals'; +import { mockReset } from 'jest-mock-extended'; + +jest.unstable_mockModule('../../src/indexer/sorobanClient.js', () => { + const mockServer = { + getLatestLedger: jest.fn(), + getEvents: jest.fn(), + }; + return { + __esModule: true, + sorobanServer: mockServer, + default: mockServer, + }; +}); + +const { default: prismaMock } = (await import('../../src/config/prisma.js')) as any; +const { sorobanServer } = (await import('../../src/indexer/sorobanClient.js')) as any; +const { tick, startPolling, stopPolling, getCursor, setCursor, resetPoller } = await import( + '../../src/indexer/poller.js' +); +const { registerEventHandler, clearHandlers, dispatch } = await import( + '../../src/indexer/registry.js' +); +const { environment } = await import('../../src/config/environment.js'); + +describe('Core Soroban Indexer Infrastructure', () => { + beforeEach(() => { + mockReset(prismaMock); + clearHandlers(); + resetPoller(); + jest.clearAllMocks(); + environment.stellar.contractId = 'C_TEST_CONTRACT_ID'; + environment.stellar.indexerStartLedger = undefined; + prismaMock.$transaction.mockImplementation(async (cb: any) => cb(prismaMock)); + }); + + afterEach(() => { + stopPolling(); + }); + + it('fails fast if STELLAR_CONTRACT_ID is unset', async () => { + environment.stellar.contractId = ''; + await expect(tick()).rejects.toThrow('STELLAR_CONTRACT_ID environment variable is unset or empty'); + await expect(startPolling()).rejects.toThrow('STELLAR_CONTRACT_ID environment variable is unset or empty'); + }); + + it('connects to RPC, fetches latest ledger, and logs decoded event without erroring', async () => { + sorobanServer.getLatestLedger.mockResolvedValue({ sequence: 100 }); + sorobanServer.getEvents.mockResolvedValue({ + events: [ + { + id: 'evt-1', + topic: [], + value: null, + ledger: 100, + txHash: 'hash-1', + }, + ], + }); + prismaMock.indexerCursor.findUnique.mockResolvedValue(null); + prismaMock.indexerEvent.findUnique.mockResolvedValue(null); + + await tick(); + + expect(sorobanServer.getLatestLedger).toHaveBeenCalled(); + expect(sorobanServer.getEvents).toHaveBeenCalledWith({ + startLedger: 100, + filters: [{ type: 'contract', contractIds: ['C_TEST_CONTRACT_ID'] }], + limit: 100, + }); + expect(getCursor()).toBe(101); + }); + + it('persists cursor after processed batch and resumes correctly', async () => { + prismaMock.indexerCursor.findUnique.mockResolvedValue({ contractId: 'C_TEST_CONTRACT_ID', lastLedger: 50 }); + sorobanServer.getLatestLedger.mockResolvedValue({ sequence: 55 }); + sorobanServer.getEvents.mockResolvedValue({ events: [] }); + + await tick(); + + expect(sorobanServer.getEvents).toHaveBeenCalledWith({ + startLedger: 50, + filters: [{ type: 'contract', contractIds: ['C_TEST_CONTRACT_ID'] }], + limit: 100, + }); + expect(prismaMock.indexerCursor.upsert).toHaveBeenCalledWith({ + where: { contractId: 'C_TEST_CONTRACT_ID' }, + update: { lastLedger: 56 }, + create: { contractId: 'C_TEST_CONTRACT_ID', lastLedger: 56 }, + }); + expect(getCursor()).toBe(56); + }); + + it('prevents raw event id from being dispatched twice via IndexerEvent replay guard', async () => { + sorobanServer.getLatestLedger.mockResolvedValue({ sequence: 10 }); + sorobanServer.getEvents.mockResolvedValue({ + events: [ + { + id: 'evt-duplicate', + topic: [], + value: null, + ledger: 10, + txHash: 'hash-dup', + }, + ], + }); + prismaMock.indexerCursor.findUnique.mockResolvedValue({ contractId: 'C_TEST_CONTRACT_ID', lastLedger: 10 }); + prismaMock.indexerEvent.findUnique.mockResolvedValue({ id: 'evt-duplicate', topic: '', ledger: 10 }); + + const handler = jest.fn(); + registerEventHandler('', handler); + + await tick(); + + expect(handler).not.toHaveBeenCalled(); + expect(prismaMock.indexerEvent.create).not.toHaveBeenCalled(); + }); + + it('skips dispatching on topic with no registered handler without throwing', async () => { + await expect( + dispatch({ + id: 'test-id', + topic: 'unregistered_topic', + ledger: 1, + txHash: 'hash', + data: { foo: 'bar' }, + }) + ).resolves.not.toThrow(); + }); + + it('logs error on bad event and continues poll loop', async () => { + sorobanServer.getLatestLedger.mockResolvedValue({ sequence: 20 }); + sorobanServer.getEvents.mockResolvedValue({ + events: [ + { + id: 'evt-bad', + topic: [], + value: null, + ledger: 20, + txHash: 'hash-bad', + }, + { + id: 'evt-good', + topic: [], + value: null, + ledger: 20, + txHash: 'hash-good', + }, + ], + }); + prismaMock.indexerCursor.findUnique.mockResolvedValue({ contractId: 'C_TEST_CONTRACT_ID', lastLedger: 20 }); + prismaMock.indexerEvent.findUnique.mockResolvedValue(null); + + const handler = jest.fn().mockImplementationOnce(() => { + throw new Error('Handler failed on bad event'); + }).mockImplementationOnce(() => {}); + registerEventHandler('', handler); + + await tick(); + + expect(handler).toHaveBeenCalledTimes(2); + expect(prismaMock.indexerEvent.create).toHaveBeenCalledTimes(1); + expect(prismaMock.indexerEvent.create).toHaveBeenCalledWith({ + data: { + id: 'evt-good', + topic: '', + ledger: 20, + }, + }); + }); +}); From 434b0353bfa74689ddd0d3f483954634cd712260 Mon Sep 17 00:00:00 2001 From: Lewechi Date: Mon, 27 Jul 2026 11:51:52 +0100 Subject: [PATCH 2/7] fix: fixes --- jest.config.js | 20 -------------------- package.json | 7 ++++--- pnpm-lock.yaml | 18 ++++++++++++++++++ tsconfig.json | 3 ++- 4 files changed, 24 insertions(+), 24 deletions(-) delete mode 100644 jest.config.js diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 5a1a4e3..0000000 --- a/jest.config.js +++ /dev/null @@ -1,20 +0,0 @@ -const jestConfig = { - preset: 'ts-jest/presets/default-esm', - testEnvironment: 'node', - extensionsToTreatAsEsm: ['.ts'], - moduleNameMapper: { - '^(\\.{1,2}/.*)\\.js$': '$1', - }, - transform: { - '^.+\\.tsx?$': [ - 'ts-jest', - { - useESM: true, - }, - ], - }, - modulePathIgnorePatterns: ['/dist/'], - roots: ['/tests/'], - setupFiles: ['/tests/jest.setup.ts'], -}; -export default jestConfig; diff --git a/package.json b/package.json index a48c39a..9a1369d 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,9 @@ "prisma:generate": "prisma generate", "prisma:migrate": "prisma migrate dev", "prisma:studio": "prisma studio", - "test": "NODE_ENV=test DATABASE_URL='postgresql://postgres:postgres@localhost:5432/postgres?schema=public' NODE_OPTIONS='--experimental-vm-modules' jest", - "test:watch": "NODE_ENV=test DATABASE_URL='postgresql://postgres:postgres@localhost:5432/postgres?schema=public' NODE_OPTIONS='--experimental-vm-modules' jest --watch", - "test:coverage": "NODE_ENV=test DATABASE_URL='postgresql://postgres:postgres@localhost:5432/postgres?schema=public' NODE_OPTIONS='--experimental-vm-modules' jest --coverage" + "test": "cross-env NODE_ENV=test DATABASE_URL='postgresql://postgres:postgres@localhost:5432/postgres?schema=public' NODE_OPTIONS='--experimental-vm-modules' jest --config jest.config.ts", + "test:watch": "cross-env NODE_ENV=test DATABASE_URL='postgresql://postgres:postgres@localhost:5432/postgres?schema=public' NODE_OPTIONS='--experimental-vm-modules' jest --config jest.config.ts --watch", + "test:coverage": "cross-env NODE_ENV=test DATABASE_URL='postgresql://postgres:postgres@localhost:5432/postgres?schema=public' NODE_OPTIONS='--experimental-vm-modules' jest --config jest.config.ts --coverage" }, "keywords": [], "author": "", @@ -59,6 +59,7 @@ "@types/urijs": "^1.19.26", "@typescript-eslint/eslint-plugin": "^8.30.1", "@typescript-eslint/parser": "^8.30.1", + "cross-env": "^10.1.0", "eslint": "^9.24.0", "eslint-config-prettier": "^10.1.2", "eslint-plugin-prettier": "^5.2.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 29385fc..0d830b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,6 +96,9 @@ importers: '@typescript-eslint/parser': specifier: ^8.30.1 version: 8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + cross-env: + specifier: ^10.1.0 + version: 10.1.0 eslint: specifier: ^9.24.0 version: 9.39.5(jiti@2.7.0) @@ -333,6 +336,9 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@epic-web/invariant@1.0.0': + resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} + '@esbuild/aix-ppc64@0.28.1': resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} @@ -1573,6 +1579,11 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cross-env@10.1.0: + resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==} + engines: {node: '>=20'} + hasBin: true + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -3680,6 +3691,8 @@ snapshots: tslib: 2.8.1 optional: true + '@epic-web/invariant@1.0.0': {} + '@esbuild/aix-ppc64@0.28.1': optional: true @@ -5047,6 +5060,11 @@ snapshots: create-require@1.1.1: {} + cross-env@10.1.0: + dependencies: + '@epic-web/invariant': 1.0.0 + cross-spawn: 7.0.6 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 diff --git a/tsconfig.json b/tsconfig.json index 45e0975..0000995 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,7 @@ "exclude": [ "node_modules", "dist", - "tests" + "tests", + "jest.config.ts" ] } \ No newline at end of file From e694c504584ed0967d0672262a664075e1b5a3f0 Mon Sep 17 00:00:00 2001 From: Lewechi Date: Mon, 27 Jul 2026 12:29:53 +0100 Subject: [PATCH 3/7] fix: migration with corect postgress database url --- .../migration.sql | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 prisma/migrations/20260727112743_add_indexer_tables/migration.sql diff --git a/prisma/migrations/20260727112743_add_indexer_tables/migration.sql b/prisma/migrations/20260727112743_add_indexer_tables/migration.sql new file mode 100644 index 0000000..41cf66a --- /dev/null +++ b/prisma/migrations/20260727112743_add_indexer_tables/migration.sql @@ -0,0 +1,41 @@ +-- CreateTable +CREATE TABLE "PaymentConfirmation" ( + "id" TEXT NOT NULL, + "invoiceId" TEXT NOT NULL, + "merchantId" TEXT NOT NULL, + "payerAddress" TEXT NOT NULL, + "txHash" TEXT, + "idempotencyKey" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "PaymentConfirmation_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "IndexerCursor" ( + "id" TEXT NOT NULL, + "contractId" TEXT NOT NULL, + "lastLedger" INTEGER NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "IndexerCursor_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "IndexerEvent" ( + "id" TEXT NOT NULL, + "topic" TEXT NOT NULL, + "ledger" INTEGER NOT NULL, + "processedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "IndexerEvent_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "PaymentConfirmation_idempotencyKey_key" ON "PaymentConfirmation"("idempotencyKey"); + +-- CreateIndex +CREATE UNIQUE INDEX "IndexerCursor_contractId_key" ON "IndexerCursor"("contractId"); + +-- AddForeignKey +ALTER TABLE "PaymentConfirmation" ADD CONSTRAINT "PaymentConfirmation_invoiceId_merchantId_fkey" FOREIGN KEY ("invoiceId", "merchantId") REFERENCES "Invoice"("id", "merchantId") ON DELETE RESTRICT ON UPDATE CASCADE; From 7cdb2ae6b88728a2fb5f3bd1180131fa123ce2a0 Mon Sep 17 00:00:00 2001 From: Lewechi Date: Mon, 27 Jul 2026 12:52:27 +0100 Subject: [PATCH 4/7] fix: fixes --- prisma.config.js | 13 -- src/app.js | 14 -- src/config/database.js | 1 - src/config/environment.js | 51 ------ src/config/prisma.js | 18 -- src/controllers/api-key.controllers.js | 57 ------ src/controllers/auth.controllers.js | 85 --------- src/controllers/index.js | 1 - src/controllers/invoice.controllers.js | 127 ------------- src/controllers/merchant.controllers.js | 112 ------------ src/controllers/pay.controllers.js | 69 ------- src/entities/index.js | 1 - src/indexer/handlers/index.js | 1 - src/indexer/poller.js | 148 --------------- src/indexer/registry.js | 15 -- src/indexer/run.js | 13 -- src/indexer/sorobanClient.js | 4 - src/indexer/types.js | 1 - src/middlewares/auth.middleware.js | 129 ------------- src/routes/auth.routes.js | 9 - src/routes/index.js | 11 -- src/routes/invoice.routes.js | 12 -- src/routes/merchant.routes.js | 16 -- src/routes/pay.routes.js | 7 - src/server.js | 15 -- src/services/api-key.services.js | 89 --------- src/services/auth.services.js | 100 ---------- src/services/email.service.js | 112 ------------ src/services/index.js | 1 - src/services/invoice-pdf.services.js | 95 ---------- src/services/invoice.services.js | 143 --------------- src/services/merchant.services.js | 191 -------------------- src/services/otp.services.js | 85 --------- src/services/pay.services.js | 93 ---------- src/services/storage/invoice-pdf.storage.js | 3 - src/utils/api-key.utils.js | 15 -- src/utils/errors.js | 9 - src/utils/invoice.validation.js | 121 ------------- src/utils/slug.js | 9 - src/utils/validation.js | 76 -------- 40 files changed, 2072 deletions(-) delete mode 100644 prisma.config.js delete mode 100644 src/app.js delete mode 100644 src/config/database.js delete mode 100644 src/config/environment.js delete mode 100644 src/config/prisma.js delete mode 100644 src/controllers/api-key.controllers.js delete mode 100644 src/controllers/auth.controllers.js delete mode 100644 src/controllers/index.js delete mode 100644 src/controllers/invoice.controllers.js delete mode 100644 src/controllers/merchant.controllers.js delete mode 100644 src/controllers/pay.controllers.js delete mode 100644 src/entities/index.js delete mode 100644 src/indexer/handlers/index.js delete mode 100644 src/indexer/poller.js delete mode 100644 src/indexer/registry.js delete mode 100644 src/indexer/run.js delete mode 100644 src/indexer/sorobanClient.js delete mode 100644 src/indexer/types.js delete mode 100644 src/middlewares/auth.middleware.js delete mode 100644 src/routes/auth.routes.js delete mode 100644 src/routes/index.js delete mode 100644 src/routes/invoice.routes.js delete mode 100644 src/routes/merchant.routes.js delete mode 100644 src/routes/pay.routes.js delete mode 100644 src/server.js delete mode 100644 src/services/api-key.services.js delete mode 100644 src/services/auth.services.js delete mode 100644 src/services/email.service.js delete mode 100644 src/services/index.js delete mode 100644 src/services/invoice-pdf.services.js delete mode 100644 src/services/invoice.services.js delete mode 100644 src/services/merchant.services.js delete mode 100644 src/services/otp.services.js delete mode 100644 src/services/pay.services.js delete mode 100644 src/services/storage/invoice-pdf.storage.js delete mode 100644 src/utils/api-key.utils.js delete mode 100644 src/utils/errors.js delete mode 100644 src/utils/invoice.validation.js delete mode 100644 src/utils/slug.js delete mode 100644 src/utils/validation.js diff --git a/prisma.config.js b/prisma.config.js deleted file mode 100644 index 3eb6195..0000000 --- a/prisma.config.js +++ /dev/null @@ -1,13 +0,0 @@ -// This file was generated by Prisma, and assumes you have installed the following: -// npm install --save-dev prisma dotenv -import 'dotenv/config'; -import { defineConfig } from 'prisma/config'; -export default defineConfig({ - schema: 'prisma/schema.prisma', - migrations: { - path: 'prisma/migrations', - }, - datasource: { - url: process.env['DATABASE_URL'], - }, -}); diff --git a/src/app.js b/src/app.js deleted file mode 100644 index 4b7c76a..0000000 --- a/src/app.js +++ /dev/null @@ -1,14 +0,0 @@ -import 'reflect-metadata'; -import express from 'express'; -import cors from 'cors'; -import helmet from 'helmet'; -import routes from './routes/index.js'; -const app = express(); -// Middleware -app.use(helmet()); -app.use(cors()); -app.use(express.json()); -app.use(express.urlencoded({ extended: true })); -// Routes -app.use('/api/v1/', routes); -export default app; diff --git a/src/config/database.js b/src/config/database.js deleted file mode 100644 index cb0ff5c..0000000 --- a/src/config/database.js +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/src/config/environment.js b/src/config/environment.js deleted file mode 100644 index d825fa5..0000000 --- a/src/config/environment.js +++ /dev/null @@ -1,51 +0,0 @@ -import dotenv from 'dotenv'; -import path from 'path'; -import { fileURLToPath } from 'url'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -// Load environment variables from .env file -dotenv.config({ path: path.join(__dirname, '../../.env') }); -const EMAIL_PROVIDERS = ['console', 'resend', 'smtp']; -const parseEmailProvider = (value) => { - const provider = value || 'console'; - if (!EMAIL_PROVIDERS.includes(provider)) { - console.warn(`Invalid EMAIL_PROVIDER "${provider}", falling back to console`); - return 'console'; - } - return provider; -}; -const parseOptionalInt = (value) => { - if (!value || value.trim() === '') - return undefined; - const parsed = parseInt(value, 10); - return Number.isNaN(parsed) ? undefined : parsed; -}; -export const environment = { - nodeEnv: process.env.NODE_ENV || 'development', - port: parseInt(process.env.PORT || '3000', 10), - jwtSecret: process.env.JWT_SECRET || 'dev-jwt-secret-change-in-production', - db: { - host: process.env.DB_HOST || 'localhost', - port: parseInt(process.env.DB_PORT || '5432', 10), - username: process.env.DB_USERNAME || 'postgres', - password: process.env.DB_PASSWORD || 'postgres', - database: process.env.DB_DATABASE || 'postgres', - }, - email: { - from: process.env.EMAIL_FROM || 'noreply@shade.local', - provider: parseEmailProvider(process.env.EMAIL_PROVIDER), - resendApiKey: process.env.RESEND_API_KEY || '', - smtp: { - host: process.env.SMTP_HOST || '', - port: parseInt(process.env.SMTP_PORT || '587', 10), - user: process.env.SMTP_USER || '', - pass: process.env.SMTP_PASS || '', - secure: process.env.SMTP_SECURE === 'true', - }, - }, - stellar: { - rpcUrl: process.env.STELLAR_RPC_URL || 'https://soroban-testnet.stellar.org', - contractId: process.env.STELLAR_CONTRACT_ID || '', - indexerStartLedger: parseOptionalInt(process.env.STELLAR_INDEXER_START_LEDGER), - }, -}; diff --git a/src/config/prisma.js b/src/config/prisma.js deleted file mode 100644 index 9703922..0000000 --- a/src/config/prisma.js +++ /dev/null @@ -1,18 +0,0 @@ -import dotenv from 'dotenv'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { PrismaClient } from '@prisma/client'; -import { PrismaPg } from '@prisma/adapter-pg'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -dotenv.config({ path: path.join(__dirname, '../../.env') }); -const prismaClientSingleton = () => { - if (process.env.NODE_ENV === 'test') - return {}; - const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); - return new PrismaClient({ adapter }); -}; -const prisma = globalThis.prisma ?? prismaClientSingleton(); -export default prisma; -if (process.env.NODE_ENV !== 'production') - globalThis.prisma = prisma; diff --git a/src/controllers/api-key.controllers.js b/src/controllers/api-key.controllers.js deleted file mode 100644 index b2a419e..0000000 --- a/src/controllers/api-key.controllers.js +++ /dev/null @@ -1,57 +0,0 @@ -import { createApiKey, listApiKeys, revokeApiKey } from '../services/api-key.services.js'; -import { AppError } from '../utils/errors.js'; -export const createApiKeyController = async (req, res) => { - const merchant = req.merchant; - if (!merchant) { - res.status(401).json({ error: 'Unauthorized' }); - return; - } - if (req.body?.label !== undefined && typeof req.body.label !== 'string') { - res.status(400).json({ error: 'label must be a string' }); - return; - } - const label = typeof req.body?.label === 'string' ? req.body.label : undefined; - try { - const apiKey = await createApiKey(merchant.id, label); - res.status(201).json(apiKey); - } - catch (error) { - if (error instanceof AppError) { - res.status(error.statusCode).json({ error: error.message }); - return; - } - res.status(500).json({ error: 'Internal Server Error' }); - } -}; -export const listApiKeysController = async (req, res) => { - const merchant = req.merchant; - if (!merchant) { - res.status(401).json({ error: 'Unauthorized' }); - return; - } - try { - const apiKeys = await listApiKeys(merchant.id); - res.status(200).json(apiKeys); - } - catch { - res.status(500).json({ error: 'Internal Server Error' }); - } -}; -export const revokeApiKeyController = async (req, res) => { - const merchant = req.merchant; - if (!merchant) { - res.status(401).json({ error: 'Unauthorized' }); - return; - } - try { - await revokeApiKey(merchant.id, req.params.id); - res.status(200).json({ message: 'API key revoked' }); - } - catch (error) { - if (error instanceof AppError) { - res.status(error.statusCode).json({ error: error.message }); - return; - } - res.status(500).json({ error: 'Internal Server Error' }); - } -}; diff --git a/src/controllers/auth.controllers.js b/src/controllers/auth.controllers.js deleted file mode 100644 index 813d6b7..0000000 --- a/src/controllers/auth.controllers.js +++ /dev/null @@ -1,85 +0,0 @@ -import { createNonce, authenticateWallet } from '../services/auth.services.js'; -import { resendEmailOtp, verifyEmailOtp } from '../services/otp.services.js'; -import { sanitizeMerchant } from '../services/merchant.services.js'; -import { AppError } from '../utils/errors.js'; -export const createNonceController = async (req, res) => { - try { - const { address } = req.body; - if (!address || typeof address !== 'string') { - res.status(400).json({ error: 'address is required' }); - return; - } - const result = await createNonce(address); - res.status(201).json(result); - } - catch (error) { - res.status(500).json({ error: 'Internal Server Error' }); - } -}; -export const verifySignatureController = async (req, res) => { - try { - const { address, nonce, signature } = req.body; - if (!address || !nonce || !signature) { - res.status(400).json({ error: 'address, nonce, and signature are required' }); - return; - } - if (typeof address !== 'string' || typeof nonce !== 'string' || typeof signature !== 'string') { - res.status(400).json({ error: 'address, nonce, and signature must be strings' }); - return; - } - const result = await authenticateWallet(address, nonce, signature); - if (!result.success) { - res.status(401).json({ error: result.reason }); - return; - } - res.status(200).json({ - accessToken: result.accessToken, - refreshToken: result.refreshToken, - merchant: result.merchant, - }); - } - catch (error) { - res.status(500).json({ error: 'Internal Server Error' }); - } -}; -export const verifyEmailController = async (req, res) => { - const merchant = req.merchant; - if (!merchant) { - res.status(401).json({ error: 'Unauthorized' }); - return; - } - const { code } = req.body; - if (!code || typeof code !== 'string') { - res.status(400).json({ error: 'code is required' }); - return; - } - try { - const updatedMerchant = await verifyEmailOtp(merchant.id, code.trim()); - res.status(200).json(sanitizeMerchant(updatedMerchant)); - } - catch (error) { - if (error instanceof AppError) { - res.status(error.statusCode).json({ error: error.message }); - return; - } - res.status(500).json({ error: 'Internal Server Error' }); - } -}; -export const resendOtpController = async (req, res) => { - const merchant = req.merchant; - if (!merchant) { - res.status(401).json({ error: 'Unauthorized' }); - return; - } - try { - await resendEmailOtp(merchant.id); - res.status(200).json({ message: 'Verification code sent' }); - } - catch (error) { - if (error instanceof AppError) { - res.status(error.statusCode).json({ error: error.message }); - return; - } - res.status(500).json({ error: 'Internal Server Error' }); - } -}; diff --git a/src/controllers/index.js b/src/controllers/index.js deleted file mode 100644 index cb0ff5c..0000000 --- a/src/controllers/index.js +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/src/controllers/invoice.controllers.js b/src/controllers/invoice.controllers.js deleted file mode 100644 index b00ee5f..0000000 --- a/src/controllers/invoice.controllers.js +++ /dev/null @@ -1,127 +0,0 @@ -import { createInvoice, getInvoice, getInvoiceWithMerchant, listInvoices, voidInvoice, } from '../services/invoice.services.js'; -import { parseInvoiceListQuery, validateCreateInvoice } from '../utils/invoice.validation.js'; -import { AppError } from '../utils/errors.js'; -import { generateInvoicePdf } from '../services/invoice-pdf.services.js'; -import { sendInvoiceEmail } from '../services/email.service.js'; -export const createInvoiceController = async (req, res) => { - const merchant = req.merchant; - if (!merchant) { - res.status(401).json({ error: 'Unauthorized' }); - return; - } - const errors = validateCreateInvoice(req.body); - if (Object.keys(errors).length > 0) { - res.status(400).json({ error: 'Validation failed', errors }); - return; - } - try { - const invoice = await createInvoice(merchant.id, req.body); - res.status(201).json(invoice); - } - catch (error) { - if (error instanceof AppError) { - res.status(error.statusCode).json({ error: error.message }); - return; - } - res.status(500).json({ error: 'Internal Server Error' }); - } -}; -export const listInvoicesController = async (req, res) => { - const merchant = req.merchant; - if (!merchant) { - res.status(401).json({ error: 'Unauthorized' }); - return; - } - const { filters, pagination, errors } = parseInvoiceListQuery(req.query); - if (Object.keys(errors).length > 0) { - res.status(400).json({ error: 'Validation failed', errors }); - return; - } - try { - const result = await listInvoices(merchant.id, filters, pagination); - res.status(200).json(result); - } - catch { - res.status(500).json({ error: 'Internal Server Error' }); - } -}; -export const getInvoiceController = async (req, res) => { - const merchant = req.merchant; - if (!merchant) { - res.status(401).json({ error: 'Unauthorized' }); - return; - } - try { - const invoice = await getInvoice(merchant.id, req.params.id); - res.status(200).json(invoice); - } - catch (error) { - if (error instanceof AppError) { - res.status(error.statusCode).json({ error: error.message }); - return; - } - res.status(500).json({ error: 'Internal Server Error' }); - } -}; -export const voidInvoiceController = async (req, res) => { - const merchant = req.merchant; - if (!merchant) { - res.status(401).json({ error: 'Unauthorized' }); - return; - } - try { - const invoice = await voidInvoice(merchant.id, req.params.id); - res.status(200).json(invoice); - } - catch (error) { - if (error instanceof AppError) { - res.status(error.statusCode).json({ error: error.message }); - return; - } - res.status(500).json({ error: 'Internal Server Error' }); - } -}; -export const getInvoicePdfController = async (req, res) => { - const merchant = req.merchant; - if (!merchant) { - res.status(401).json({ error: 'Unauthorized' }); - return; - } - try { - const invoice = await getInvoiceWithMerchant(merchant.id, req.params.id); - const pdf = await generateInvoicePdf(invoice, invoice.merchant); - res.setHeader('Content-Type', 'application/pdf'); - res.setHeader('Content-Disposition', `attachment; filename="invoice-${invoice.paymentSlug}.pdf"`); - res.status(200).send(pdf); - } - catch (error) { - if (error instanceof AppError) { - res.status(error.statusCode).json({ error: error.message }); - return; - } - res.status(500).json({ error: 'Internal Server Error' }); - } -}; -export const sendInvoiceController = async (req, res) => { - const merchant = req.merchant; - if (!merchant) { - res.status(401).json({ error: 'Unauthorized' }); - return; - } - try { - const invoice = await getInvoiceWithMerchant(merchant.id, req.params.id); - if (!invoice.email) { - res.status(400).json({ error: 'Invoice has no email on file' }); - return; - } - await sendInvoiceEmail(invoice, invoice.merchant); - res.status(200).json({ message: 'Invoice email sent' }); - } - catch (error) { - if (error instanceof AppError) { - res.status(error.statusCode).json({ error: error.message }); - return; - } - res.status(500).json({ error: 'Internal Server Error' }); - } -}; diff --git a/src/controllers/merchant.controllers.js b/src/controllers/merchant.controllers.js deleted file mode 100644 index 3c34600..0000000 --- a/src/controllers/merchant.controllers.js +++ /dev/null @@ -1,112 +0,0 @@ -import { createMerchant, getMerchant, listMerchants, registerMerchant, getMyProfile, updateMyProfile, generateMerchantSigningKey, } from '../services/merchant.services.js'; -import { validateRegisterMerchant, validateUpdateMerchant } from '../utils/validation.js'; -import { AppError } from '../utils/errors.js'; -export const createMerchantController = async (req, res) => { - try { - const merchant = await createMerchant(req.body); - res.status(201).json(merchant); - } - catch (error) { - res.status(500).json({ error: 'Internal Server Error' }); - } -}; -export const getMerchantController = async (req, res) => { - try { - const merchant = await getMerchant(Number(req.params.id)); - res.status(200).json(merchant); - } - catch (error) { - res.status(500).json({ error: 'Internal Server Error' }); - } -}; -export const listMerchantsController = async (req, res) => { - try { - const merchants = await listMerchants(Number(req.query.limit), Number(req.query.offset)); - res.status(200).json(merchants); - } - catch (error) { - res.status(500).json({ error: 'Internal Server Error' }); - } -}; -export const registerMerchantController = async (req, res) => { - const merchant = req.merchant; - if (!merchant) { - res.status(401).json({ error: 'Unauthorized' }); - return; - } - const errors = validateRegisterMerchant(req.body); - if (Object.keys(errors).length > 0) { - res.status(400).json({ error: 'Validation failed', errors }); - return; - } - try { - const profile = await registerMerchant(merchant.id, req.body); - res.status(200).json(profile); - } - catch (error) { - if (error instanceof AppError) { - res.status(error.statusCode).json({ error: error.message }); - return; - } - res.status(500).json({ error: 'Internal Server Error' }); - } -}; -export const getMyProfileController = async (req, res) => { - const merchant = req.merchant; - if (!merchant) { - res.status(401).json({ error: 'Unauthorized' }); - return; - } - try { - const profile = await getMyProfile(merchant.id); - res.status(200).json(profile); - } - catch (error) { - if (error instanceof AppError) { - res.status(error.statusCode).json({ error: error.message }); - return; - } - res.status(500).json({ error: 'Internal Server Error' }); - } -}; -export const generateSigningKeyController = async (req, res) => { - const merchant = req.merchant; - if (!merchant) { - res.status(401).json({ error: 'Unauthorized' }); - return; - } - try { - const keys = await generateMerchantSigningKey(merchant.id); - res.status(201).json(keys); - } - catch (error) { - if (error instanceof AppError) { - res.status(error.statusCode).json({ error: error.message }); - return; - } - res.status(500).json({ error: 'Internal Server Error' }); - } -}; -export const updateMyProfileController = async (req, res) => { - const merchant = req.merchant; - if (!merchant) { - res.status(401).json({ error: 'Unauthorized' }); - return; - } - const errors = validateUpdateMerchant(req.body); - if (Object.keys(errors).length > 0) { - res.status(400).json({ error: 'Validation failed', errors }); - return; - } - try { - const profile = await updateMyProfile(merchant.id, req.body); - res.status(200).json(profile); - } - catch (error) { - if (error instanceof AppError) { - res.status(error.statusCode).json({ error: error.message }); - return; - } - res.status(500).json({ error: 'Internal Server Error' }); - } -}; diff --git a/src/controllers/pay.controllers.js b/src/controllers/pay.controllers.js deleted file mode 100644 index 78836c7..0000000 --- a/src/controllers/pay.controllers.js +++ /dev/null @@ -1,69 +0,0 @@ -import { resolveInvoiceBySlug, confirmPayment, getInvoiceForPdfBySlug, } from '../services/pay.services.js'; -import { AppError } from '../utils/errors.js'; -import { generateInvoicePdf } from '../services/invoice-pdf.services.js'; -export const resolveInvoiceController = async (req, res) => { - try { - const { slug } = req.params; - const invoice = await resolveInvoiceBySlug(slug); - res.status(200).json(invoice); - } - catch (error) { - if (error instanceof AppError) { - if (error.statusCode === 410 && error.message === 'expired') { - res.status(410).json({ reason: 'expired' }); - return; - } - res.status(error.statusCode).json({ error: error.message }); - return; - } - res.status(500).json({ error: 'Internal Server Error' }); - } -}; -export const getInvoicePdfController = async (req, res) => { - try { - const { slug } = req.params; - const invoice = await getInvoiceForPdfBySlug(slug); - const pdf = await generateInvoicePdf(invoice, invoice.merchant); - res.setHeader('Content-Type', 'application/pdf'); - res.setHeader('Content-Disposition', `attachment; filename="invoice-${invoice.paymentSlug}.pdf"`); - res.status(200).send(pdf); - } - catch (error) { - if (error instanceof AppError) { - if (error.statusCode === 410 && error.message === 'expired') { - res.status(410).json({ reason: 'expired' }); - return; - } - res.status(error.statusCode).json({ error: error.message }); - return; - } - res.status(500).json({ error: 'Internal Server Error' }); - } -}; -export const confirmPaymentController = async (req, res) => { - try { - const { slug } = req.params; - const { payerAddress, txHash } = req.body; - if (!payerAddress || typeof payerAddress !== 'string') { - res.status(400).json({ error: 'payerAddress is required and must be a string' }); - return; - } - if (txHash !== undefined && typeof txHash !== 'string') { - res.status(400).json({ error: 'txHash must be a string if provided' }); - return; - } - await confirmPayment(slug, payerAddress, txHash); - res.status(202).json({ message: 'Payment confirmation received' }); - } - catch (error) { - if (error instanceof AppError) { - if (error.statusCode === 410 && error.message === 'expired') { - res.status(410).json({ reason: 'expired' }); - return; - } - res.status(error.statusCode).json({ error: error.message }); - return; - } - res.status(500).json({ error: 'Internal Server Error' }); - } -}; diff --git a/src/entities/index.js b/src/entities/index.js deleted file mode 100644 index cb0ff5c..0000000 --- a/src/entities/index.js +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/src/indexer/handlers/index.js b/src/indexer/handlers/index.js deleted file mode 100644 index cb0ff5c..0000000 --- a/src/indexer/handlers/index.js +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/src/indexer/poller.js b/src/indexer/poller.js deleted file mode 100644 index bda7fdd..0000000 --- a/src/indexer/poller.js +++ /dev/null @@ -1,148 +0,0 @@ -import { scValToNative } from '@stellar/stellar-sdk'; -import prisma from '../config/prisma.js'; -import { environment } from '../config/environment.js'; -import { sorobanServer } from './sorobanClient.js'; -import { dispatch } from './registry.js'; -let isRunning = false; -let cursor; -function decodeTopic(val) { - if (!val) - return ''; - try { - const native = scValToNative(val); - if (typeof native === 'symbol') { - return native.description ?? native.toString(); - } - return String(native); - } - catch { - return 'unknown_topic'; - } -} -export async function tick() { - try { - const contractId = environment.stellar.contractId; - if (!contractId || contractId.trim() === '') { - throw new Error('STELLAR_CONTRACT_ID environment variable is unset or empty'); - } - const latestLedgerResp = await sorobanServer.getLatestLedger(); - const latestLedger = latestLedgerResp.sequence; - if (cursor === undefined) { - const cursorRecord = await prisma.indexerCursor.findUnique({ - where: { contractId }, - }); - if (cursorRecord?.lastLedger != null) { - cursor = cursorRecord.lastLedger; - } - else if (environment.stellar.indexerStartLedger != null) { - cursor = environment.stellar.indexerStartLedger; - } - else { - cursor = latestLedger; - } - console.log(`Indexer initialized with cursor at ledger ${cursor}`); - } - const currentCursor = cursor ?? latestLedger; - cursor = currentCursor; - if (currentCursor > latestLedger) { - return; - } - const eventsResp = await sorobanServer.getEvents({ - startLedger: currentCursor, - filters: [{ type: 'contract', contractIds: [contractId] }], - limit: 100, - }); - const events = eventsResp.events || []; - const processedIds = []; - for (const event of events) { - try { - const existing = await prisma.indexerEvent.findUnique({ - where: { id: event.id }, - }); - if (existing) { - continue; - } - const topicVal = event.topic && event.topic.length > 0 ? event.topic[0] : undefined; - const decodedTopic = decodeTopic(topicVal); - let decodedValue = null; - try { - decodedValue = event.value ? scValToNative(event.value) : null; - } - catch { - decodedValue = null; - } - console.log(`Decoded event [${event.id}] - topic: ${decodedTopic}, value:`, decodedValue); - await dispatch({ - id: event.id, - topic: decodedTopic, - ledger: event.ledger, - txHash: event.txHash, - data: decodedValue, - }); - processedIds.push({ - id: event.id, - topic: decodedTopic, - ledger: event.ledger, - }); - } - catch (err) { - console.error(`Error processing event ${event.id}:`, err); - } - } - const nextCursor = events.length === 100 && events[events.length - 1] - ? events[events.length - 1].ledger + 1 - : latestLedger + 1; - await prisma.$transaction(async (tx) => { - for (const item of processedIds) { - await tx.indexerEvent.create({ - data: { - id: item.id, - topic: item.topic, - ledger: item.ledger, - }, - }); - } - await tx.indexerCursor.upsert({ - where: { contractId }, - update: { lastLedger: nextCursor }, - create: { contractId, lastLedger: nextCursor }, - }); - }); - cursor = nextCursor; - } - catch (error) { - console.error('Error in poller tick:', error); - if (!environment.stellar.contractId || environment.stellar.contractId.trim() === '') { - throw error; - } - } -} -export async function startPolling(intervalMs = 6000) { - const contractId = environment.stellar.contractId; - if (!contractId || contractId.trim() === '') { - throw new Error('STELLAR_CONTRACT_ID environment variable is unset or empty'); - } - if (isRunning) - return; - isRunning = true; - console.log(`Starting Soroban indexer poller for contract ${contractId}...`); - while (isRunning) { - await tick(); - if (!isRunning) - break; - await new Promise((resolve) => setTimeout(resolve, intervalMs)); - } -} -export function stopPolling() { - isRunning = false; -} -export function getCursor() { - return cursor; -} -export function setCursor(val) { - cursor = val; -} -export function resetPoller() { - stopPolling(); - cursor = undefined; -} diff --git a/src/indexer/registry.js b/src/indexer/registry.js deleted file mode 100644 index 50ed99b..0000000 --- a/src/indexer/registry.js +++ /dev/null @@ -1,15 +0,0 @@ -const handlers = new Map(); -export function registerEventHandler(topic, handler) { - handlers.set(topic, handler); -} -export async function dispatch(event) { - const handler = handlers.get(event.topic); - if (!handler) { - console.log(`No handler registered for topic "${event.topic}", skipping.`); - return; - } - await handler(event); -} -export function clearHandlers() { - handlers.clear(); -} diff --git a/src/indexer/run.js b/src/indexer/run.js deleted file mode 100644 index 674a4ec..0000000 --- a/src/indexer/run.js +++ /dev/null @@ -1,13 +0,0 @@ -import { startPolling, stopPolling } from './poller.js'; -process.on('SIGINT', () => { - console.log('Received SIGINT, shutting down indexer...'); - stopPolling(); -}); -process.on('SIGTERM', () => { - console.log('Received SIGTERM, shutting down indexer...'); - stopPolling(); -}); -startPolling().catch((error) => { - console.error('Fatal error starting Soroban indexer:', error); - process.exit(1); -}); diff --git a/src/indexer/sorobanClient.js b/src/indexer/sorobanClient.js deleted file mode 100644 index b39bc62..0000000 --- a/src/indexer/sorobanClient.js +++ /dev/null @@ -1,4 +0,0 @@ -import { rpc } from '@stellar/stellar-sdk'; -import { environment } from '../config/environment.js'; -export const sorobanServer = new rpc.Server(environment.stellar.rpcUrl); -export default sorobanServer; diff --git a/src/indexer/types.js b/src/indexer/types.js deleted file mode 100644 index cb0ff5c..0000000 --- a/src/indexer/types.js +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/src/middlewares/auth.middleware.js b/src/middlewares/auth.middleware.js deleted file mode 100644 index 3684a7e..0000000 --- a/src/middlewares/auth.middleware.js +++ /dev/null @@ -1,129 +0,0 @@ -import jwt from 'jsonwebtoken'; -import prisma from '../config/prisma.js'; -import { environment } from '../config/environment.js'; -import { authenticateApiKey } from '../services/api-key.services.js'; -import { isApiKeyToken } from '../utils/api-key.utils.js'; -const extractBearerToken = (req) => { - const authHeader = req.headers.authorization; - if (!authHeader || !authHeader.startsWith('Bearer ')) { - return null; - } - const token = authHeader.slice('Bearer '.length).trim(); - return token || null; -}; -const authenticateRefreshToken = async (token) => { - const session = await prisma.refreshToken.findUnique({ - where: { token }, - include: { merchant: true }, - }); - if (!session || session.expiresAt.getTime() < Date.now()) { - return null; - } - return session.merchant; -}; -const authenticateJwt = async (token) => { - try { - const payload = jwt.verify(token, environment.jwtSecret); - if (!payload.sub) { - return null; - } - return prisma.merchant.findUnique({ where: { id: payload.sub } }); - } - catch { - return null; - } -}; -const resolveMerchantFromToken = async (token) => { - if (isApiKeyToken(token)) { - return authenticateApiKey(token); - } - if (token.split('.').length === 3) { - return authenticateJwt(token); - } - return authenticateRefreshToken(token); -}; -/** - * Authenticates API key bearer tokens, updates lastUsedAt, and attaches the merchant. - */ -export const apiKeyAuth = async (req, res, next) => { - try { - const token = extractBearerToken(req); - if (!token) { - res.status(401).json({ error: 'Authentication required' }); - return; - } - if (!isApiKeyToken(token)) { - res.status(401).json({ error: 'Invalid or expired token' }); - return; - } - const merchant = await authenticateApiKey(token); - if (!merchant) { - res.status(401).json({ error: 'Invalid or expired token' }); - return; - } - req.merchant = merchant; - next(); - } - catch { - res.status(500).json({ error: 'Internal Server Error' }); - } -}; -/** - * Authenticates a merchant using refresh tokens or JWT access tokens only. - * API keys are rejected to prevent key-management operations via API keys. - */ -export const authenticateSessionOnly = async (req, res, next) => { - try { - const token = extractBearerToken(req); - if (!token) { - res.status(401).json({ error: 'Authentication required' }); - return; - } - if (isApiKeyToken(token)) { - res.status(401).json({ error: 'Invalid or expired token' }); - return; - } - const merchant = token.split('.').length === 3 - ? await authenticateJwt(token) - : await authenticateRefreshToken(token); - if (!merchant) { - res.status(401).json({ error: 'Invalid or expired token' }); - return; - } - req.merchant = merchant; - next(); - } - catch { - res.status(500).json({ error: 'Internal Server Error' }); - } -}; -/** - * Authenticates a merchant from a bearer token. - * - * Accepts JWT access tokens (signed with `JWT_SECRET`), refresh session tokens, - * or API keys. The resolved Merchant is attached to `req.merchant` on success. - * - * Responds with 401 when the `Authorization: Bearer ` header is missing - * or malformed (`Authentication required`), or when the token is invalid, - * expired, or references a merchant that no longer exists - * (`Invalid or expired token`). - */ -export const authenticateMerchant = async (req, res, next) => { - try { - const token = extractBearerToken(req); - if (!token) { - res.status(401).json({ error: 'Authentication required' }); - return; - } - const merchant = await resolveMerchantFromToken(token); - if (!merchant) { - res.status(401).json({ error: 'Invalid or expired token' }); - return; - } - req.merchant = merchant; - next(); - } - catch { - res.status(500).json({ error: 'Internal Server Error' }); - } -}; diff --git a/src/routes/auth.routes.js b/src/routes/auth.routes.js deleted file mode 100644 index ff579f3..0000000 --- a/src/routes/auth.routes.js +++ /dev/null @@ -1,9 +0,0 @@ -import { Router } from 'express'; -import { createNonceController, verifySignatureController, verifyEmailController, resendOtpController, } from '../controllers/auth.controllers.js'; -import { authenticateMerchant } from '../middlewares/auth.middleware.js'; -const router = Router(); -router.post('/nonce', createNonceController); -router.post('/verify', verifySignatureController); -router.post('/verify-email', authenticateMerchant, verifyEmailController); -router.post('/resend-otp', authenticateMerchant, resendOtpController); -export default router; diff --git a/src/routes/index.js b/src/routes/index.js deleted file mode 100644 index 2209819..0000000 --- a/src/routes/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import merchantRoutes from './merchant.routes.js'; -import authRoutes from './auth.routes.js'; -import invoiceRoutes from './invoice.routes.js'; -import payRoutes from './pay.routes.js'; -import { Router } from 'express'; -const router = Router(); -router.use('/merchants', merchantRoutes); -router.use('/auth', authRoutes); -router.use('/invoices', invoiceRoutes); -router.use('/pay', payRoutes); -export default router; diff --git a/src/routes/invoice.routes.js b/src/routes/invoice.routes.js deleted file mode 100644 index 1bd704a..0000000 --- a/src/routes/invoice.routes.js +++ /dev/null @@ -1,12 +0,0 @@ -import { Router } from 'express'; -import { createInvoiceController, getInvoiceController, getInvoicePdfController, listInvoicesController, sendInvoiceController, voidInvoiceController, } from '../controllers/invoice.controllers.js'; -import { authenticateMerchant } from '../middlewares/auth.middleware.js'; -const router = Router(); -router.use(authenticateMerchant); -router.post('/', createInvoiceController); -router.get('/', listInvoicesController); -router.get('/:id', getInvoiceController); -router.get('/:id/pdf', getInvoicePdfController); -router.post('/:id/send', sendInvoiceController); -router.patch('/:id/void', voidInvoiceController); -export default router; diff --git a/src/routes/merchant.routes.js b/src/routes/merchant.routes.js deleted file mode 100644 index 721bba7..0000000 --- a/src/routes/merchant.routes.js +++ /dev/null @@ -1,16 +0,0 @@ -import { Router } from 'express'; -import { createMerchantController, getMerchantController, listMerchantsController, registerMerchantController, getMyProfileController, updateMyProfileController, generateSigningKeyController, } from '../controllers/merchant.controllers.js'; -import { createApiKeyController, listApiKeysController, revokeApiKeyController, } from '../controllers/api-key.controllers.js'; -import { authenticateMerchant, authenticateSessionOnly } from '../middlewares/auth.middleware.js'; -const router = Router(); -router.post('/register', authenticateMerchant, registerMerchantController); -router.get('/me', authenticateMerchant, getMyProfileController); -router.patch('/me', authenticateMerchant, updateMyProfileController); -router.post('/signing-key', authenticateSessionOnly, generateSigningKeyController); -router.post('/api-keys', authenticateSessionOnly, createApiKeyController); -router.get('/api-keys', authenticateSessionOnly, listApiKeysController); -router.delete('/api-keys/:id', authenticateSessionOnly, revokeApiKeyController); -router.post('/', createMerchantController); -router.get('/:id', getMerchantController); -router.get('/', listMerchantsController); -export default router; diff --git a/src/routes/pay.routes.js b/src/routes/pay.routes.js deleted file mode 100644 index 54f2861..0000000 --- a/src/routes/pay.routes.js +++ /dev/null @@ -1,7 +0,0 @@ -import { Router } from 'express'; -import { resolveInvoiceController, confirmPaymentController, getInvoicePdfController, } from '../controllers/pay.controllers.js'; -const router = Router(); -router.get('/:slug', resolveInvoiceController); -router.get('/:slug/pdf', getInvoicePdfController); -router.post('/:slug/confirm', confirmPaymentController); -export default router; diff --git a/src/server.js b/src/server.js deleted file mode 100644 index b08c2d4..0000000 --- a/src/server.js +++ /dev/null @@ -1,15 +0,0 @@ -import app from './app.js'; -import { environment } from './config/environment.js'; -const startServer = async () => { - try { - // Start Express server - app.listen(environment.port, () => { - console.log(`Server running on port ${environment.port} in ${environment.nodeEnv} mode`); - }); - } - catch (error) { - console.error('Error starting server:', error); - process.exit(1); - } -}; -startServer(); diff --git a/src/services/api-key.services.js b/src/services/api-key.services.js deleted file mode 100644 index b02ca85..0000000 --- a/src/services/api-key.services.js +++ /dev/null @@ -1,89 +0,0 @@ -import prisma from '../config/prisma.js'; -import { AppError } from '../utils/errors.js'; -import { generateApiKeyMaterial, hashApiKey, MAX_ACTIVE_API_KEYS } from '../utils/api-key.utils.js'; -const toApiKeySummary = (apiKey) => ({ - id: apiKey.id, - prefix: apiKey.prefix ?? '', - label: apiKey.name, - lastUsedAt: apiKey.lastUsedAt, - createdAt: apiKey.createdAt, -}); -const activeApiKeyWhere = (merchantId) => ({ - merchantId, - revokedAt: null, - OR: [{ expiresAt: null }, { expiresAt: { gt: new Date() } }], -}); -export const createApiKey = async (merchantId, label) => { - const { rawKey, prefix, keyHash } = generateApiKeyMaterial(); - const normalizedLabel = label?.trim() || null; - const apiKey = await prisma.$transaction(async (tx) => { - const activeKeys = await tx.apiKey.count({ - where: activeApiKeyWhere(merchantId), - }); - if (activeKeys >= MAX_ACTIVE_API_KEYS) { - throw new AppError(400, `Maximum of ${MAX_ACTIVE_API_KEYS} active API keys allowed`); - } - return tx.apiKey.create({ - data: { - merchantId, - keyHash, - prefix, - name: normalizedLabel, - }, - }); - }); - return { - ...toApiKeySummary(apiKey), - key: rawKey, - }; -}; -export const listApiKeys = async (merchantId) => { - const apiKeys = await prisma.apiKey.findMany({ - where: { - merchantId, - revokedAt: null, - }, - orderBy: { createdAt: 'desc' }, - select: { - id: true, - prefix: true, - name: true, - lastUsedAt: true, - createdAt: true, - }, - }); - return apiKeys.map(toApiKeySummary); -}; -export const revokeApiKey = async (merchantId, keyId) => { - const apiKey = await prisma.apiKey.findFirst({ - where: { id: keyId, merchantId }, - }); - if (!apiKey) { - throw new AppError(404, 'API key not found'); - } - if (apiKey.revokedAt) { - throw new AppError(400, 'API key already revoked'); - } - await prisma.apiKey.update({ - where: { id: keyId }, - data: { revokedAt: new Date() }, - }); -}; -export const authenticateApiKey = async (rawKey) => { - const keyHash = hashApiKey(rawKey); - const apiKey = await prisma.apiKey.findUnique({ - where: { keyHash }, - include: { merchant: true }, - }); - if (!apiKey || apiKey.revokedAt) { - return null; - } - if (apiKey.expiresAt && apiKey.expiresAt.getTime() < Date.now()) { - return null; - } - await prisma.apiKey.update({ - where: { id: apiKey.id }, - data: { lastUsedAt: new Date() }, - }); - return apiKey.merchant; -}; diff --git a/src/services/auth.services.js b/src/services/auth.services.js deleted file mode 100644 index f45a531..0000000 --- a/src/services/auth.services.js +++ /dev/null @@ -1,100 +0,0 @@ -import crypto from 'node:crypto'; -import jwt from 'jsonwebtoken'; -import { Keypair } from '@stellar/stellar-sdk'; -import prisma from '../config/prisma.js'; -import { environment } from '../config/environment.js'; -const NONCE_EXPIRY_MS = 5 * 60 * 1000; -const REFRESH_TOKEN_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000; -export function buildChallengeMessage(address, nonce, createdAt) { - return [ - 'Shade Authentication', - `Address: ${address}`, - `Nonce: ${nonce}`, - `Timestamp: ${createdAt.toISOString()}`, - ].join('\n'); -} -export async function createNonce(address) { - const nonce = crypto.randomUUID(); - const createdAt = new Date(); - const expiresAt = new Date(createdAt.getTime() + NONCE_EXPIRY_MS); - const message = buildChallengeMessage(address, nonce, createdAt); - const authNonce = await prisma.authNonce.create({ - data: { address, nonce, message, expiresAt }, - }); - return { nonce: authNonce.nonce, message: authNonce.message, expiresAt: authNonce.expiresAt }; -} -export async function verifySignature(address, nonce, rawSignature) { - const authNonce = await prisma.authNonce.findUnique({ where: { nonce } }); - if (!authNonce) { - return { valid: false, reason: 'Nonce not found' }; - } - if (authNonce.address !== address) { - return { valid: false, reason: 'Address mismatch' }; - } - if (authNonce.usedAt) { - return { valid: false, reason: 'Nonce already used' }; - } - if (new Date() > authNonce.expiresAt) { - return { valid: false, reason: 'Nonce expired' }; - } - const message = buildChallengeMessage(address, authNonce.nonce, authNonce.createdAt); - const messageBytes = Buffer.from(message, 'utf-8'); - const signatureBytes = Buffer.from(rawSignature, 'hex'); - let isValid; - try { - const keypair = Keypair.fromPublicKey(address); - isValid = keypair.verify(messageBytes, signatureBytes); - } - catch { - return { valid: false, reason: 'Invalid address or signature format' }; - } - if (!isValid) { - return { valid: false, reason: 'Signature verification failed' }; - } - await prisma.authNonce.update({ - where: { id: authNonce.id }, - data: { usedAt: new Date() }, - }); - return { valid: true, reason: null }; -} -export async function upsertMerchant(address) { - const existing = await prisma.merchant.findFirst({ where: { address } }); - if (existing) { - return existing; - } - const merchantId = crypto.randomInt(100_000, 999_999); - const merchant = await prisma.merchant.create({ - data: { merchantId, address }, - }); - return merchant; -} -export function issueAccessToken(merchantId, address) { - return jwt.sign({ sub: merchantId, address }, environment.jwtSecret, { expiresIn: '15m' }); -} -export async function issueRefreshToken(merchantId) { - const token = crypto.randomUUID(); - const expiresAt = new Date(Date.now() + REFRESH_TOKEN_EXPIRY_MS); - await prisma.refreshToken.create({ - data: { merchantId, token, expiresAt }, - }); - return token; -} -export async function authenticateWallet(address, nonce, signature) { - const verification = await verifySignature(address, nonce, signature); - if (!verification.valid) { - return { success: false, reason: verification.reason }; - } - const merchant = await upsertMerchant(address); - const accessToken = issueAccessToken(merchant.id, merchant.address); - const refreshToken = await issueRefreshToken(merchant.id); - return { - success: true, - accessToken, - refreshToken, - merchant: { - id: merchant.id, - address: merchant.address, - isRegistered: merchant.registered, - }, - }; -} diff --git a/src/services/email.service.js b/src/services/email.service.js deleted file mode 100644 index 977b3c2..0000000 --- a/src/services/email.service.js +++ /dev/null @@ -1,112 +0,0 @@ -import nodemailer from 'nodemailer'; -import { Resend } from 'resend'; -import { environment } from '../config/environment.js'; -import { generateInvoicePdf } from './invoice-pdf.services.js'; -const escapeHtml = (value) => value - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -const buildOtpEmailContent = (firstName, code) => { - const safeFirstName = escapeHtml(firstName); - const subject = 'Verify your Shade email'; - const html = ` -

Hi ${safeFirstName},

-

Your email verification code is:

-

${code}

-

This code expires in 10 minutes.

- `.trim(); - const text = `Hi ${firstName},\n\nYour verification code is: ${code}\n\nThis code expires in 10 minutes.`; - return { subject, html, text }; -}; -const sendViaResend = async (to, subject, html, attachments) => { - const resend = new Resend(environment.email.resendApiKey); - const { error } = await resend.emails.send({ - from: environment.email.from, - to, - subject, - html, - attachments: attachments?.map(({ filename, content }) => ({ filename, content })), - }); - if (error) { - throw new Error(`Failed to send email via Resend: ${error.message}`); - } -}; -const sendViaSmtp = async (to, subject, html, text, attachments) => { - const transporter = nodemailer.createTransport({ - host: environment.email.smtp.host, - port: environment.email.smtp.port, - secure: environment.email.smtp.secure, - auth: { - user: environment.email.smtp.user, - pass: environment.email.smtp.pass, - }, - }); - await transporter.sendMail({ - from: environment.email.from, - to, - subject, - html, - text, - attachments, - }); -}; -/** - * Delivers a one-time verification code to the merchant's email address. - */ -export const sendOtp = async (to, code, firstName) => { - const { subject, html, text } = buildOtpEmailContent(firstName, code); - switch (environment.email.provider) { - case 'resend': - await sendViaResend(to, subject, html); - return; - case 'smtp': - await sendViaSmtp(to, subject, html, text); - return; - case 'console': - default: - console.log(`[OTP] Verification code ${code} sent to ${to} for ${firstName}`); - } -}; -const buildInvoiceEmailContent = (invoice, merchant) => { - const merchantName = escapeHtml(merchant.businessName || 'Your merchant'); - const description = escapeHtml(invoice.description); - const subject = `Invoice from ${merchant.businessName || 'Shade'}: ${invoice.description}`; - const html = ` -

Hi,

-

${merchantName} has sent you an invoice for ${description}.

-

Amount: ${invoice.amount.toString()} ${escapeHtml(invoice.token)}

-

Status: ${invoice.status}

-

Your invoice is attached as a PDF.

- `.trim(); - const text = `Hi,\n\n${merchant.businessName || 'Your merchant'} has sent you an invoice for ${invoice.description}.\n\nAmount: ${invoice.amount.toString()} ${invoice.token}\nStatus: ${invoice.status}\n\nYour invoice is attached as a PDF.`; - return { subject, html, text }; -}; -/** - * Emails the invoice to `invoice.email` with a freshly generated PDF attached. - * No-ops (does not throw) when the invoice has no email on file — callers - * that need to surface that as a user-facing error (e.g. the /send route) - * should check `invoice.email` before calling this. - */ -export const sendInvoiceEmail = async (invoice, merchant) => { - if (!invoice.email) { - return; - } - const pdf = await generateInvoicePdf(invoice, merchant); - const { subject, html, text } = buildInvoiceEmailContent(invoice, merchant); - const attachments = [ - { filename: `invoice-${invoice.paymentSlug}.pdf`, content: pdf }, - ]; - switch (environment.email.provider) { - case 'resend': - await sendViaResend(invoice.email, subject, html, attachments); - return; - case 'smtp': - await sendViaSmtp(invoice.email, subject, html, text, attachments); - return; - case 'console': - default: - console.log(`[Invoice email] Invoice ${invoice.paymentSlug} (${pdf.length} byte PDF) sent`); - } -}; diff --git a/src/services/index.js b/src/services/index.js deleted file mode 100644 index cb0ff5c..0000000 --- a/src/services/index.js +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/src/services/invoice-pdf.services.js b/src/services/invoice-pdf.services.js deleted file mode 100644 index ac3357c..0000000 --- a/src/services/invoice-pdf.services.js +++ /dev/null @@ -1,95 +0,0 @@ -import PDFDocument from 'pdfkit'; -const FIXED_FIAT = 'FIXED_FIAT'; -// merchant.logo is a free-form string (set via the merchant profile API). Only a -// data: URI can be embedded without giving this "pure" renderer a network -// dependency, so a plain image URL is intentionally skipped rather than fetched. -const DATA_URI_IMAGE = /^data:image\/(png|jpe?g);base64,([a-z0-9+/=]+)$/i; -const decodeLogo = (logo) => { - if (!logo) - return null; - const match = DATA_URI_IMAGE.exec(logo.trim()); - if (!match) - return null; - try { - return Buffer.from(match[2], 'base64'); - } - catch { - return null; - } -}; -const formatDate = (date) => { - if (!date) - return '-'; - return date - .toISOString() - .replace('T', ' ') - .replace(/\.\d+Z$/, ' UTC'); -}; -const formatFiatAmount = (fiatAmount, fiatDecimals, fiatCurrency) => { - const decimals = Math.max(fiatDecimals, 0); - const divisor = 10n ** BigInt(decimals); - const whole = fiatAmount / divisor; - const fraction = fiatAmount % divisor; - if (decimals === 0) { - return `${whole.toString()} ${fiatCurrency}`; - } - const fractionStr = fraction.toString().padStart(decimals, '0'); - return `${whole.toString()}.${fractionStr} ${fiatCurrency}`; -}; -const drawField = (doc, label, value) => { - doc.font('Helvetica-Bold').fontSize(10).text(label, { continued: true }); - doc.font('Helvetica').fontSize(10).text(` ${value}`); - doc.moveDown(0.5); -}; -/** - * Renders an invoice + merchant pair to a PDF buffer. Pure and side-effect - * free: no database access, no filesystem or network writes. Callers are - * responsible for fetching the records; this only formats what it's given. - */ -export const generateInvoicePdf = (invoice, merchant) => { - return new Promise((resolve, reject) => { - const doc = new PDFDocument({ size: 'A4', margin: 50 }); - const chunks = []; - doc.on('data', chunk => chunks.push(chunk)); - doc.on('end', () => resolve(Buffer.concat(chunks))); - doc.on('error', reject); - const logoBuffer = decodeLogo(merchant.logo); - if (logoBuffer) { - try { - doc.image(logoBuffer, { fit: [80, 80] }); - doc.moveDown(); - } - catch (err) { - // Corrupt/undecodable image data — skip it rather than fail the render, - // but log so real failures (not just bad merchant uploads) stay visible. - console.error(`Failed to embed invoice logo for merchant ${merchant.id}`, err); - } - } - doc - .font('Helvetica-Bold') - .fontSize(18) - .text(merchant.businessName || 'Invoice'); - doc.moveDown(); - doc.font('Helvetica-Bold').fontSize(14).text('Invoice'); - doc.moveDown(0.5); - doc.font('Helvetica').fontSize(11).text(invoice.description); - doc.moveDown(); - drawField(doc, 'Amount:', `${invoice.amount.toString()} ${invoice.token}`); - if (invoice.pricingMode === FIXED_FIAT && - invoice.fiatAmount !== null && - invoice.fiatCurrency !== null && - invoice.fiatDecimals !== null) { - drawField(doc, 'Fiat amount:', formatFiatAmount(invoice.fiatAmount, invoice.fiatDecimals, invoice.fiatCurrency)); - } - drawField(doc, 'Status:', invoice.status); - drawField(doc, 'Payment link:', invoice.paymentSlug); - drawField(doc, 'Created:', formatDate(invoice.createdAt)); - if (invoice.datePaid) { - drawField(doc, 'Paid:', formatDate(invoice.datePaid)); - } - if (invoice.payer) { - drawField(doc, 'Payer address:', invoice.payer); - } - doc.end(); - }); -}; diff --git a/src/services/invoice.services.js b/src/services/invoice.services.js deleted file mode 100644 index 9187f92..0000000 --- a/src/services/invoice.services.js +++ /dev/null @@ -1,143 +0,0 @@ -import prisma from '../config/prisma.js'; -import { AppError } from '../utils/errors.js'; -import { generatePaymentSlug } from '../utils/slug.js'; -import { parseAmount, } from '../utils/invoice.validation.js'; -const SLUG_MAX_RETRIES = 5; -// String constants matching the Prisma `Status` enum. Defined locally so this -// module never imports a runtime value from `@prisma/client` (the generated -// client is mocked in tests and not generated in CI). -const InvoiceStatus = { - DRAFT: 'DRAFT', - PENDING: 'PENDING', - PAID: 'PAID', - CANCELLED: 'CANCELLED', -}; -/** - * Public-facing view of an invoice. `amount` is serialized to a string because - * `BigInt` is not JSON-serializable. - */ -export const sanitizeInvoice = (invoice) => ({ - id: invoice.id, - paymentSlug: invoice.paymentSlug, - description: invoice.description, - amount: invoice.amount.toString(), - token: invoice.token, - status: invoice.status, - merchantId: invoice.merchantId, - email: invoice.email, - expiresAt: invoice.expiresAt, - datePaid: invoice.datePaid, - createdAt: invoice.createdAt, - updatedAt: invoice.updatedAt, -}); -const isUniqueSlugError = (error) => { - if (typeof error !== 'object' || error === null) - return false; - const { code, meta } = error; - return code === 'P2002' && Array.isArray(meta?.target) && meta.target.includes('paymentSlug'); -}; -export const createInvoice = async (merchantId, data) => { - const amount = parseAmount(data.amount); - if (amount === null) { - throw new AppError(400, 'amount must be a positive integer'); - } - const status = data.isDraft ? InvoiceStatus.DRAFT : InvoiceStatus.PENDING; - const expiresAt = data.expiresAt ? new Date(data.expiresAt) : null; - for (let attempt = 0; attempt < SLUG_MAX_RETRIES; attempt++) { - try { - const invoice = await prisma.invoice.create({ - data: { - merchantId, - description: data.description.trim(), - amount, - token: data.token.trim(), - email: data.payerEmail?.trim() ?? null, - expiresAt, - status, - paymentSlug: generatePaymentSlug(), - }, - }); - return sanitizeInvoice(invoice); - } - catch (error) { - if (isUniqueSlugError(error) && attempt < SLUG_MAX_RETRIES - 1) { - continue; - } - throw error; - } - } - throw new AppError(500, 'Failed to generate a unique payment slug'); -}; -export const listInvoices = async (merchantId, filters, pagination) => { - const where = { merchantId }; - if (filters.status) { - where.status = filters.status; - } - if (filters.token) { - where.token = filters.token; - } - if (filters.startDate || filters.endDate) { - where.createdAt = {}; - if (filters.startDate) - where.createdAt.gte = filters.startDate; - if (filters.endDate) - where.createdAt.lte = filters.endDate; - } - const [invoices, total] = await Promise.all([ - prisma.invoice.findMany({ - where, - take: pagination.limit, - skip: pagination.offset, - orderBy: { createdAt: 'desc' }, - }), - prisma.invoice.count({ where }), - ]); - return { - data: invoices.map(sanitizeInvoice), - pagination: { - limit: pagination.limit, - offset: pagination.offset, - total, - }, - }; -}; -export const getInvoice = async (merchantId, id) => { - const invoice = await prisma.invoice.findFirst({ - where: { id, merchantId }, - }); - if (!invoice) { - throw new AppError(404, 'Invoice not found'); - } - return sanitizeInvoice(invoice); -}; -/** - * Fetches the raw invoice + merchant records, scoped to the owning merchant, - * for the PDF/email flows that need fields beyond the sanitized public view - * (payer address, fiat breakdown, merchant logo). - */ -export const getInvoiceWithMerchant = async (merchantId, id) => { - const invoice = await prisma.invoice.findFirst({ - where: { id, merchantId }, - include: { merchant: true }, - }); - if (!invoice) { - throw new AppError(404, 'Invoice not found'); - } - return invoice; -}; -export const voidInvoice = async (merchantId, id) => { - const invoice = await prisma.invoice.findFirst({ - where: { id, merchantId }, - }); - if (!invoice) { - throw new AppError(404, 'Invoice not found'); - } - if (invoice.status !== InvoiceStatus.PENDING) { - throw new AppError(400, 'Only pending invoices can be voided'); - } - const updated = await prisma.invoice.update({ - where: { id: invoice.id }, - data: { status: InvoiceStatus.CANCELLED }, - }); - return sanitizeInvoice(updated); -}; diff --git a/src/services/merchant.services.js b/src/services/merchant.services.js deleted file mode 100644 index 64693a4..0000000 --- a/src/services/merchant.services.js +++ /dev/null @@ -1,191 +0,0 @@ -import prisma from '../config/prisma.js'; -import { AppError } from '../utils/errors.js'; -import { generateOtp, hashOtp } from './otp.services.js'; -import { sendOtp } from './email.service.js'; -import { Keypair } from '@stellar/stellar-sdk'; -const OTP_EXPIRY_MS = 10 * 60 * 1000; -/** - * Returns a public-facing view of a merchant. Built as an allow-list so that - * any sensitive fields added to the model later are never exposed by default. - */ -export const sanitizeMerchant = (merchant) => ({ - id: merchant.id, - merchantId: merchant.merchantId, - email: merchant.email, - address: merchant.address, - account: merchant.account, - merchantKey: merchant.merchantKey, - firstName: merchant.firstName, - lastName: merchant.lastName, - businessName: merchant.businessName, - category: merchant.category, - description: merchant.description, - logo: merchant.logo, - webhook: merchant.webhook, - active: merchant.active, - verified: merchant.verified, - emailVerified: merchant.emailVerified, - registered: merchant.registered, - createdAt: merchant.createdAt, - updatedAt: merchant.updatedAt, -}); -export const createMerchant = async (merchantData) => { - try { - const merchant = await prisma.merchant.create({ - data: merchantData, - }); - return merchant; - } - catch (error) { - throw error; - } -}; -export const getMerchant = async (merchantId) => { - try { - const merchant = await prisma.merchant.findUnique({ - where: { - merchantId: merchantId, - }, - }); - return merchant; - } - catch (error) { - throw error; - } -}; -export const listMerchants = async (limit, offset) => { - try { - const merchants = await prisma.merchant.findMany({ - take: limit, - skip: offset, - }); - return merchants; - } - catch (error) { - throw error; - } -}; -/** - * Completes a merchant's profile after wallet authentication. - * - * Enforces that the email is unique across merchants and that the profile has - * not already been completed, persists the profile data, resets email - * verification, and triggers an OTP email. - */ -export const registerMerchant = async (merchantId, data) => { - const merchant = await prisma.merchant.findUnique({ - where: { id: merchantId }, - }); - if (!merchant) { - throw new AppError(404, 'Merchant not found'); - } - if (merchant.registered) { - throw new AppError(409, 'Profile already set up'); - } - const normalizedEmail = data.email.trim().toLowerCase(); - const existingEmail = await prisma.merchant.findFirst({ - where: { - email: normalizedEmail, - NOT: { id: merchantId }, - }, - }); - if (existingEmail) { - throw new AppError(409, 'Email already registered'); - } - const code = generateOtp(); - const emailOtp = await hashOtp(code); - const emailOtpExpiresAt = new Date(Date.now() + OTP_EXPIRY_MS); - const updatedMerchant = await prisma.merchant.update({ - where: { id: merchantId }, - data: { - firstName: data.firstName.trim(), - lastName: data.lastName.trim(), - email: normalizedEmail, - businessName: data.businessName.trim(), - category: data.category.trim(), - description: data.description.trim(), - logo: data.logo?.trim() ?? null, - emailVerified: false, - registered: true, - emailOtp, - emailOtpExpiresAt, - }, - }); - try { - await sendOtp(normalizedEmail, code, data.firstName.trim()); - } - catch (err) { - console.error('Failed to send OTP email after registration', err); - } - return sanitizeMerchant(updatedMerchant); -}; -/** - * Returns the authenticated merchant's own profile. - */ -export const getMyProfile = async (id) => { - const merchant = await prisma.merchant.findUnique({ where: { id } }); - if (!merchant) { - throw new AppError(404, 'Merchant not found'); - } - return sanitizeMerchant(merchant); -}; -/** - * Generates a fresh Ed25519 signing keypair for the merchant. - * - * Persists ONLY the hex-encoded 32-byte public key to `Merchant.merchantKey`, - * overwriting any previous value (unconditional generate-and-replace). Returns - * both halves; the hex-encoded 32-byte private key is returned exactly once and - * is never written to the database or logged. - * - * Uploading the public key on-chain (`set_merchant_key`) and signing invoices - * with the private key are done client/SDK-side and are out of scope here. - */ -export const generateMerchantSigningKey = async (id) => { - const merchant = await prisma.merchant.findUnique({ where: { id } }); - if (!merchant) { - throw new AppError(404, 'Merchant not found'); - } - const keypair = Keypair.random(); - const publicKey = Buffer.from(keypair.rawPublicKey()).toString('hex'); - const privateKey = Buffer.from(keypair.rawSecretKey()).toString('hex'); - // Optimistic concurrency: only replace the key we just read. If a concurrent - // rotation already changed it, no row matches and we reject rather than return - // a private key whose public half is no longer the one persisted. - const { count } = await prisma.merchant.updateMany({ - where: { id, merchantKey: merchant.merchantKey }, - data: { merchantKey: publicKey }, - }); - if (count !== 1) { - throw new AppError(409, 'Signing key was changed concurrently; please retry'); - } - // Audit only — never include the private key here. - console.info(`[merchant] signing key ${merchant.merchantKey ? 'rotated' : 'created'} for merchant ${id}`); - return { publicKey, privateKey }; -}; -/** - * Partially updates the authenticated merchant's editable profile fields. - * - * Only fields present in `data` are written. Strings are trimmed; an empty - * `logo`/`webhook` is normalized to null so the merchant can clear them. - * Non-editable fields are never read here, so they cannot be changed. - */ -export const updateMyProfile = async (id, data) => { - const updateData = {}; - const textFields = ['firstName', 'lastName', 'businessName', 'category', 'description']; - for (const field of textFields) { - const value = data[field]; - if (value !== undefined) { - updateData[field] = value.trim(); - } - } - if (data.logo !== undefined) { - const logo = typeof data.logo === 'string' ? data.logo.trim() : data.logo; - updateData.logo = logo ? logo : null; - } - if (data.webhook !== undefined) { - const webhook = typeof data.webhook === 'string' ? data.webhook.trim() : data.webhook; - updateData.webhook = webhook ? webhook : null; - } - const updated = await prisma.merchant.update({ where: { id }, data: updateData }); - return sanitizeMerchant(updated); -}; diff --git a/src/services/otp.services.js b/src/services/otp.services.js deleted file mode 100644 index 3ae9e2d..0000000 --- a/src/services/otp.services.js +++ /dev/null @@ -1,85 +0,0 @@ -import { randomInt } from 'node:crypto'; -import bcrypt from 'bcrypt'; -import prisma from '../config/prisma.js'; -import { AppError } from '../utils/errors.js'; -import { sendOtp } from './email.service.js'; -const OTP_LENGTH = 6; -const OTP_EXPIRY_MS = 10 * 60 * 1000; -const OTP_RESEND_COOLDOWN_MS = 60 * 1000; -const BCRYPT_ROUNDS = 10; -export const generateOtp = () => { - const min = 10 ** (OTP_LENGTH - 1); - const max = 10 ** OTP_LENGTH - 1; - return randomInt(min, max + 1).toString(); -}; -export const hashOtp = async (code) => bcrypt.hash(code, BCRYPT_ROUNDS); -export const verifyOtpHash = async (code, hash) => bcrypt.compare(code, hash); -const getLastOtpSentAt = (expiresAt) => new Date(expiresAt.getTime() - OTP_EXPIRY_MS); -/** - * Generates a 6-digit OTP, stores its bcrypt hash with a 10-minute expiry, - * and sends the code to the merchant's email. - */ -export const issueEmailOtp = async (merchant) => { - const code = generateOtp(); - const emailOtp = await hashOtp(code); - const emailOtpExpiresAt = new Date(Date.now() + OTP_EXPIRY_MS); - await prisma.merchant.update({ - where: { id: merchant.id }, - data: { emailOtp, emailOtpExpiresAt }, - }); - await sendOtp(merchant.email, code, merchant.firstName?.trim() || 'there'); -}; -/** - * Validates the submitted OTP against the stored hash and marks the email verified. - */ -export const verifyEmailOtp = async (merchantId, code) => { - const merchant = await prisma.merchant.findUnique({ - where: { id: merchantId }, - }); - if (!merchant?.emailOtp || !merchant.emailOtpExpiresAt) { - throw new AppError(400, 'Invalid verification code'); - } - if (merchant.emailOtpExpiresAt.getTime() < Date.now()) { - throw new AppError(400, 'Code expired'); - } - const isValid = await verifyOtpHash(code, merchant.emailOtp); - if (!isValid) { - throw new AppError(400, 'Invalid verification code'); - } - return prisma.merchant.update({ - where: { id: merchantId }, - data: { - emailVerified: true, - emailOtp: null, - emailOtpExpiresAt: null, - }, - }); -}; -/** - * Re-generates and re-sends the email OTP, rate-limited to one request per minute. - */ -export const resendEmailOtp = async (merchantId) => { - const merchant = await prisma.merchant.findUnique({ - where: { id: merchantId }, - }); - if (!merchant) { - throw new AppError(404, 'Merchant not found'); - } - if (!merchant.registered || !merchant.email) { - throw new AppError(400, 'Registration incomplete'); - } - if (merchant.emailVerified) { - throw new AppError(400, 'Email already verified'); - } - if (merchant.emailOtpExpiresAt) { - const lastSentAt = getLastOtpSentAt(merchant.emailOtpExpiresAt); - if (Date.now() - lastSentAt.getTime() < OTP_RESEND_COOLDOWN_MS) { - throw new AppError(429, 'Please wait before requesting a new code'); - } - } - await issueEmailOtp({ - id: merchant.id, - email: merchant.email, - firstName: merchant.firstName, - }); -}; diff --git a/src/services/pay.services.js b/src/services/pay.services.js deleted file mode 100644 index d033d41..0000000 --- a/src/services/pay.services.js +++ /dev/null @@ -1,93 +0,0 @@ -import prisma from '../config/prisma.js'; -import { AppError } from '../utils/errors.js'; -const InvoiceStatus = { - DRAFT: 'DRAFT', - PENDING: 'PENDING', - PAID: 'PAID', - CANCELLED: 'CANCELLED', - REFUNDED: 'REFUNDED', -}; -const assertInvoiceVisible = (invoice) => { - if (invoice.status === InvoiceStatus.CANCELLED || - invoice.status === InvoiceStatus.PAID || - invoice.status === InvoiceStatus.REFUNDED) { - throw new AppError(410, 'Invoice is no longer available'); - } - if (invoice.expiresAt && invoice.expiresAt < new Date()) { - throw new AppError(410, 'expired'); - } -}; -export const resolveInvoiceBySlug = async (slug) => { - const invoice = await prisma.invoice.findUnique({ - where: { paymentSlug: slug }, - select: { - paymentSlug: true, - description: true, - amount: true, - token: true, - status: true, - expiresAt: true, - pricingMode: true, - merchant: { - select: { - businessName: true, - }, - }, - }, - }); - if (!invoice) { - throw new AppError(404, 'Invoice not found'); - } - assertInvoiceVisible(invoice); - return { - slug: invoice.paymentSlug, - description: invoice.description, - amount: invoice.amount.toString(), - token: invoice.token, - status: invoice.status, - merchantName: invoice.merchant.businessName, - expiresAt: invoice.expiresAt, - pricingMode: invoice.pricingMode, - }; -}; -/** - * Fetches the full invoice + merchant records for a publicly visible invoice, - * applying the same 404/410 visibility rules as `resolveInvoiceBySlug`. Used - * by the public PDF download route, which needs raw fields (payer, dates, - * fiat breakdown, logo) rather than the trimmed public-facing view. - */ -export const getInvoiceForPdfBySlug = async (slug) => { - const invoice = await prisma.invoice.findUnique({ - where: { paymentSlug: slug }, - include: { merchant: true }, - }); - if (!invoice) { - throw new AppError(404, 'Invoice not found'); - } - assertInvoiceVisible(invoice); - return invoice; -}; -export const confirmPayment = async (slug, payerAddress, txHash) => { - return await prisma.$transaction(async (tx) => { - const invoice = await tx.invoice.findUnique({ - where: { paymentSlug: slug }, - }); - if (!invoice) { - throw new AppError(404, 'Invoice not found'); - } - assertInvoiceVisible(invoice); - const idempotencyKey = `${invoice.id}-${payerAddress}-${txHash || 'none'}`; - const confirmation = await tx.paymentConfirmation.upsert({ - where: { idempotencyKey }, - update: {}, - create: { - invoiceId: invoice.id, - merchantId: invoice.merchantId, - payerAddress, - txHash: txHash || null, - idempotencyKey, - }, - }); - return confirmation; - }); -}; diff --git a/src/services/storage/invoice-pdf.storage.js b/src/services/storage/invoice-pdf.storage.js deleted file mode 100644 index 2849575..0000000 --- a/src/services/storage/invoice-pdf.storage.js +++ /dev/null @@ -1,3 +0,0 @@ -export const mockInvoicePdfStorage = { - upload: async (key) => ({ url: `mock://invoice-pdfs/${key}` }), -}; diff --git a/src/utils/api-key.utils.js b/src/utils/api-key.utils.js deleted file mode 100644 index 2d237ad..0000000 --- a/src/utils/api-key.utils.js +++ /dev/null @@ -1,15 +0,0 @@ -import crypto from 'node:crypto'; -export const API_KEY_PREFIX = 'sk_live_'; -export const API_KEY_RANDOM_LENGTH = 32; -export const API_KEY_DISPLAY_PREFIX_LENGTH = 8; -export const MAX_ACTIVE_API_KEYS = 10; -const KEY_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; -export const isApiKeyToken = (token) => token.startsWith(API_KEY_PREFIX); -export const hashApiKey = (rawKey) => crypto.createHash('sha256').update(rawKey).digest('hex'); -export const generateApiKeyMaterial = () => { - const randomPart = Array.from({ length: API_KEY_RANDOM_LENGTH }, () => KEY_ALPHABET[crypto.randomInt(0, KEY_ALPHABET.length)]).join(''); - const rawKey = `${API_KEY_PREFIX}${randomPart}`; - const prefix = `${API_KEY_PREFIX}${randomPart.slice(0, API_KEY_DISPLAY_PREFIX_LENGTH)}`; - const keyHash = hashApiKey(rawKey); - return { rawKey, prefix, keyHash }; -}; diff --git a/src/utils/errors.js b/src/utils/errors.js deleted file mode 100644 index c69500e..0000000 --- a/src/utils/errors.js +++ /dev/null @@ -1,9 +0,0 @@ -export class AppError extends Error { - statusCode; - constructor(statusCode, message) { - super(message); - this.statusCode = statusCode; - this.name = 'AppError'; - Object.setPrototypeOf(this, AppError.prototype); - } -} diff --git a/src/utils/invoice.validation.js b/src/utils/invoice.validation.js deleted file mode 100644 index 6c8fb23..0000000 --- a/src/utils/invoice.validation.js +++ /dev/null @@ -1,121 +0,0 @@ -const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; -const POSITIVE_INTEGER_REGEX = /^\d+$/; -// String constants matching the Prisma `Status` enum. Defined locally so this -// module never imports a runtime value from `@prisma/client` (the generated -// client is mocked in tests and not generated in CI). -const INVOICE_STATUSES = [ - 'DRAFT', - 'PENDING', - 'PAID', - 'CANCELLED', - 'REFUNDED', - 'PARTIALLY_REFUNDED', - 'PARTIALLY_PAID', -]; -export const DEFAULT_LIMIT = 20; -export const MAX_LIMIT = 100; -const isNonEmptyString = (value) => typeof value === 'string' && value.trim().length > 0; -/** - * Parses and validates a positive integer amount supplied as a number or a - * numeric string. Returns the BigInt value, or null when invalid. - */ -export const parseAmount = (value) => { - if (typeof value === 'number') { - if (!Number.isInteger(value) || value <= 0) - return null; - return BigInt(value); - } - if (typeof value === 'string' && POSITIVE_INTEGER_REGEX.test(value.trim())) { - const parsed = BigInt(value.trim()); - return parsed > 0n ? parsed : null; - } - return null; -}; -export const validateCreateInvoice = (body) => { - const errors = {}; - const payload = (body ?? {}); - if (!isNonEmptyString(payload.description)) { - errors.description = 'description is required'; - } - if (parseAmount(payload.amount) === null) { - errors.amount = 'amount must be a positive integer'; - } - if (!isNonEmptyString(payload.token)) { - errors.token = 'token must be a non-empty string'; - } - if (payload.payerEmail !== undefined && - payload.payerEmail !== null && - !(isNonEmptyString(payload.payerEmail) && - EMAIL_REGEX.test(payload.payerEmail.trim()))) { - errors.payerEmail = 'payerEmail must be a valid email'; - } - if (payload.expiresAt !== undefined && payload.expiresAt !== null) { - const date = new Date(payload.expiresAt); - if (Number.isNaN(date.getTime())) { - errors.expiresAt = 'expiresAt must be a valid date'; - } - } - if (payload.isDraft !== undefined && typeof payload.isDraft !== 'boolean') { - errors.isDraft = 'isDraft must be a boolean'; - } - return errors; -}; -/** - * Parses list query parameters into typed filters and pagination, clamping the - * page size to [1, MAX_LIMIT] and defaulting to DEFAULT_LIMIT. - */ -export const parseInvoiceListQuery = (query) => { - const errors = {}; - const filters = {}; - if (query.status !== undefined) { - const status = String(query.status).toUpperCase(); - if (INVOICE_STATUSES.includes(status)) { - filters.status = status; - } - else { - errors.status = `status must be one of ${INVOICE_STATUSES.join(', ')}`; - } - } - if (isNonEmptyString(query.token)) { - filters.token = query.token.trim(); - } - if (query.startDate !== undefined) { - const date = new Date(String(query.startDate)); - if (Number.isNaN(date.getTime())) { - errors.startDate = 'startDate must be a valid date'; - } - else { - filters.startDate = date; - } - } - if (query.endDate !== undefined) { - const date = new Date(String(query.endDate)); - if (Number.isNaN(date.getTime())) { - errors.endDate = 'endDate must be a valid date'; - } - else { - filters.endDate = date; - } - } - let limit = DEFAULT_LIMIT; - if (query.limit !== undefined) { - const parsed = Number(query.limit); - if (!Number.isFinite(parsed) || parsed < 1) { - errors.limit = 'limit must be a positive number'; - } - else { - limit = Math.min(Math.floor(parsed), MAX_LIMIT); - } - } - let offset = 0; - if (query.offset !== undefined) { - const parsed = Number(query.offset); - if (!Number.isFinite(parsed) || parsed < 0) { - errors.offset = 'offset must be a non-negative number'; - } - else { - offset = Math.floor(parsed); - } - } - return { filters, pagination: { limit, offset }, errors }; -}; diff --git a/src/utils/slug.js b/src/utils/slug.js deleted file mode 100644 index 200ebdf..0000000 --- a/src/utils/slug.js +++ /dev/null @@ -1,9 +0,0 @@ -import { randomBytes } from 'crypto'; -/** - * Generates a url-safe, collision-resistant payment slug. - * - * Uses base64url encoding (characters A-Z, a-z, 0-9, `-`, `_`) so the slug can - * be embedded directly in a payment URL without escaping. 12 random bytes yield - * 16 characters and ~96 bits of entropy. - */ -export const generatePaymentSlug = () => randomBytes(12).toString('base64url'); diff --git a/src/utils/validation.js b/src/utils/validation.js deleted file mode 100644 index 07f3d92..0000000 --- a/src/utils/validation.js +++ /dev/null @@ -1,76 +0,0 @@ -const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; -const REQUIRED_FIELDS = [ - 'firstName', - 'lastName', - 'email', - 'businessName', - 'category', - 'description', -]; -const isNonEmptyString = (value) => typeof value === 'string' && value.trim().length > 0; -export const validateRegisterMerchant = (body) => { - const errors = {}; - const payload = (body ?? {}); - for (const field of REQUIRED_FIELDS) { - if (!isNonEmptyString(payload[field])) { - errors[field] = `${field} is required`; - } - } - if (isNonEmptyString(payload.email) && !EMAIL_REGEX.test(payload.email.trim())) { - errors.email = 'A valid email is required'; - } - if (payload.logo !== undefined && typeof payload.logo !== 'string') { - errors.logo = 'logo must be a string'; - } - return errors; -}; -const EDITABLE_MERCHANT_FIELDS = [ - 'firstName', - 'lastName', - 'businessName', - 'category', - 'description', - 'logo', - 'webhook', -]; -const UPDATE_REQUIRED_TEXT_FIELDS = [ - 'firstName', - 'lastName', - 'businessName', - 'category', - 'description', -]; -const isValidHttpsUrl = (value) => { - try { - return new URL(value).protocol === 'https:'; - } - catch { - return false; - } -}; -export const validateUpdateMerchant = (body) => { - const errors = {}; - const payload = (body ?? {}); - const present = EDITABLE_MERCHANT_FIELDS.filter(field => payload[field] !== undefined); - if (present.length === 0) { - errors._empty = 'At least one valid field is required'; - return errors; - } - for (const field of UPDATE_REQUIRED_TEXT_FIELDS) { - if (payload[field] !== undefined && !isNonEmptyString(payload[field])) { - errors[field] = `${field} must be a non-empty string`; - } - } - if (payload.logo !== undefined && payload.logo !== null && typeof payload.logo !== 'string') { - errors.logo = 'logo must be a string or null'; - } - if (payload.webhook !== undefined && payload.webhook !== null) { - if (typeof payload.webhook !== 'string') { - errors.webhook = 'webhook must be a string or null'; - } - else if (payload.webhook.trim().length > 0 && !isValidHttpsUrl(payload.webhook.trim())) { - errors.webhook = 'webhook must be a valid HTTPS URL'; - } - } - return errors; -}; From 9c9fdc48d2e9310cae57a67dbc1335432f62655d Mon Sep 17 00:00:00 2001 From: Lewechi Date: Mon, 27 Jul 2026 18:51:27 +0100 Subject: [PATCH 5/7] fix: fixes --- package-lock.json | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/package-lock.json b/package-lock.json index 04ad715..bd1737c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,8 +37,10 @@ "@types/node": "^22.19.11", "@types/nodemailer": "^8.0.1", "@types/supertest": "^6.0.3", + "@types/urijs": "^1.19.26", "@typescript-eslint/eslint-plugin": "^8.30.1", "@typescript-eslint/parser": "^8.30.1", + "cross-env": "^10.1.0", "eslint": "^9.24.0", "eslint-config-prettier": "^10.1.2", "eslint-plugin-prettier": "^5.2.6", @@ -705,6 +707,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", @@ -2754,6 +2763,13 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/urijs": { + "version": "1.19.26", + "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.26.tgz", + "integrity": "sha512-wkXrVzX5yoqLnndOwFsieJA7oKM8cNkOKJtf/3vVGSUFkWDKZvFHpIl9Pvqb/T9UsawBBFMTTD8xu7sK5MWuvg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -4277,6 +4293,24 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "devOptional": true }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", From b2d03d033c62d3285a6164f9ee74a984037a5ef9 Mon Sep 17 00:00:00 2001 From: Lewechi Date: Tue, 28 Jul 2026 20:20:58 +0100 Subject: [PATCH 6/7] fix --- .prettierrc | 2 +- eslint-report.json | 2419 ++++++++++++++++++ eslint.config.cjs | 20 +- src/config/prisma.ts | 1 + src/controllers/auth.controllers.ts | 4 +- src/controllers/merchant.controllers.ts | 6 +- src/indexer/poller.ts | 4 +- src/indexer/run.ts | 2 +- src/services/api-key.services.ts | 2 +- src/services/merchant.services.ts | 42 +- src/services/pay.services.ts | 2 +- tests/__mocks__/prisma.ts | 8 +- tests/integration/api-key.routes.test.ts | 1 - tests/integration/auth.middleware.test.ts | 1 - tests/integration/auth.routes.test.ts | 6 +- tests/integration/merchant.profile.test.ts | 21 +- tests/integration/merchant.routes.test.ts | 108 +- tests/jest.setup.ts | 8 +- tests/unit/auth.services.test.ts | 5 +- tests/unit/indexer.test.ts | 42 +- tests/unit/invoice.schema.test.ts | 2 +- tests/unit/merchant.profile.services.test.ts | 1 - tests/unit/merchant.services.test.ts | 129 +- tests/unit/subscription.schema.test.ts | 26 +- tsconfig.eslint.json | 5 + tsconfig.json | 14 +- 26 files changed, 2671 insertions(+), 210 deletions(-) create mode 100644 eslint-report.json create mode 100644 tsconfig.eslint.json diff --git a/.prettierrc b/.prettierrc index e559e47..691d897 100644 --- a/.prettierrc +++ b/.prettierrc @@ -7,4 +7,4 @@ "bracketSpacing": true, "arrowParens": "avoid", "endOfLine": "lf" -} \ No newline at end of file +} diff --git a/eslint-report.json b/eslint-report.json new file mode 100644 index 0000000..cde5d94 --- /dev/null +++ b/eslint-report.json @@ -0,0 +1,2419 @@ +[ + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\eslint.config.cjs", + "messages": [ + { + "ruleId": "@typescript-eslint/no-require-imports", + "severity": 2, + "message": "A `require()` style import is forbidden.", + "line": 1, + "column": 24, + "nodeType": "CallExpression", + "messageId": "noRequireImports", + "endLine": 1, + "endColumn": 51 + }, + { + "ruleId": "no-undef", + "severity": 2, + "message": "'require' is not defined.", + "line": 1, + "column": 24, + "nodeType": "Identifier", + "messageId": "undef", + "endLine": 1, + "endColumn": 31 + }, + { + "ruleId": "@typescript-eslint/no-require-imports", + "severity": 2, + "message": "A `require()` style import is forbidden.", + "line": 2, + "column": 12, + "nodeType": "CallExpression", + "messageId": "noRequireImports", + "endLine": 2, + "endColumn": 33 + }, + { + "ruleId": "no-undef", + "severity": 2, + "message": "'require' is not defined.", + "line": 2, + "column": 12, + "nodeType": "Identifier", + "messageId": "undef", + "endLine": 2, + "endColumn": 19 + }, + { + "ruleId": "@typescript-eslint/no-require-imports", + "severity": 2, + "message": "A `require()` style import is forbidden.", + "line": 3, + "column": 18, + "nodeType": "CallExpression", + "messageId": "noRequireImports", + "endLine": 3, + "endColumn": 46 + }, + { + "ruleId": "no-undef", + "severity": 2, + "message": "'require' is not defined.", + "line": 3, + "column": 18, + "nodeType": "Identifier", + "messageId": "undef", + "endLine": 3, + "endColumn": 25 + }, + { + "ruleId": "no-undef", + "severity": 2, + "message": "'module' is not defined.", + "line": 8, + "column": 1, + "nodeType": "Identifier", + "messageId": "undef", + "endLine": 8, + "endColumn": 7 + } + ], + "suppressedMessages": [], + "errorCount": 7, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "const { FlatCompat } = require('@eslint/eslintrc');\nconst js = require('@eslint/js');\nconst tseslint = require('typescript-eslint');\n\n// Create compatibility layer between new flat config and traditional config formats\nconst compat = new FlatCompat();\n\nmodule.exports = [\n js.configs.recommended,\n ...tseslint.configs.recommended,\n ...compat.config({\n extends: ['prettier'],\n plugins: ['prettier'],\n }),\n {\n languageOptions: {\n ecmaVersion: 'latest',\n sourceType: 'module',\n parser: tseslint.parser,\n parserOptions: {\n project: './tsconfig.eslint.json',\n },\n },\n files: ['**/*.ts', '**/*.js'],\n ignores: [\n 'node_modules/**',\n 'dist/**',\n 'build/**',\n 'coverage/**',\n '**/*.d.ts',\n 'eslint.config.cjs',\n ],\n rules: {\n 'prettier/prettier': 'error',\n '@typescript-eslint/explicit-function-return-type': 'warn',\n '@typescript-eslint/explicit-module-boundary-types': 'warn', \n '@typescript-eslint/no-explicit-any': 'warn',\n '@typescript-eslint/no-unused-vars': ['error', { \n 'argsIgnorePattern': '^_',\n 'varsIgnorePattern': '^_',\n }],\n 'no-console': ['warn', { allow: ['warn', 'error'] }],\n 'no-duplicate-imports': 'error',\n 'no-unused-expressions': 'error',\n 'prefer-const': 'error',\n },\n },\n {\n files: ['**/*.test.ts', '**/*.spec.ts', 'tests/**/*.ts'],\n rules: {\n '@typescript-eslint/no-explicit-any': 'off',\n 'no-console': 'off',\n },\n }\n];", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\jest.config.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\prisma.config.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\app.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\config\\database.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\config\\environment.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\config\\prisma.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 12, + "column": 34, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 12, + "endColumn": 36 + }, + { + "ruleId": "no-var", + "severity": 2, + "message": "Unexpected var, use let or const instead.", + "line": 19, + "column": 3, + "nodeType": "VariableDeclaration", + "messageId": "unexpectedVar", + "endLine": 19, + "endColumn": 68 + } + ], + "suppressedMessages": [], + "errorCount": 1, + "fatalErrorCount": 0, + "warningCount": 1, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import dotenv from 'dotenv';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { PrismaClient } from '@prisma/client';\nimport { PrismaPg } from '@prisma/adapter-pg';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\ndotenv.config({ path: path.join(__dirname, '../../.env') });\n\nconst prismaClientSingleton = () => {\n if (process.env.NODE_ENV === 'test') return {} as PrismaClient;\n const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! });\n return new PrismaClient({ adapter });\n};\n\ndeclare global {\n var prisma: undefined | ReturnType;\n}\n\nconst prisma = globalThis.prisma ?? prismaClientSingleton();\n\nexport default prisma;\n\nif (process.env.NODE_ENV !== 'production') globalThis.prisma = prisma;\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\controllers\\api-key.controllers.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\controllers\\auth.controllers.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 7, + "column": 74, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 7, + "endColumn": 76 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 7, + "column": 74, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 7, + "endColumn": 76 + }, + { + "ruleId": "@typescript-eslint/no-unused-vars", + "severity": 2, + "message": "'error' is defined but never used.", + "line": 16, + "column": 12, + "nodeType": null, + "messageId": "unusedVar", + "endLine": 16, + "endColumn": 17 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 21, + "column": 78, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 21, + "endColumn": 80 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 21, + "column": 78, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 21, + "endColumn": 80 + }, + { + "ruleId": "@typescript-eslint/no-unused-vars", + "severity": 2, + "message": "'error' is defined but never used.", + "line": 45, + "column": 12, + "nodeType": null, + "messageId": "unusedVar", + "endLine": 45, + "endColumn": 17 + } + ], + "suppressedMessages": [], + "errorCount": 2, + "fatalErrorCount": 0, + "warningCount": 4, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import { Request, Response } from 'express';\nimport { createNonce, authenticateWallet } from '../services/auth.services.js';\nimport { resendEmailOtp, verifyEmailOtp } from '../services/otp.services.js';\nimport { sanitizeMerchant } from '../services/merchant.services.js';\nimport { AppError } from '../utils/errors.js';\n\nexport const createNonceController = async (req: Request, res: Response) => {\n try {\n const { address } = req.body;\n if (!address || typeof address !== 'string') {\n res.status(400).json({ error: 'address is required' });\n return;\n }\n const result = await createNonce(address);\n res.status(201).json(result);\n } catch (error) {\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n\nexport const verifySignatureController = async (req: Request, res: Response) => {\n try {\n const { address, nonce, signature } = req.body;\n if (!address || !nonce || !signature) {\n res.status(400).json({ error: 'address, nonce, and signature are required' });\n return;\n }\n if (typeof address !== 'string' || typeof nonce !== 'string' || typeof signature !== 'string') {\n res.status(400).json({ error: 'address, nonce, and signature must be strings' });\n return;\n }\n\n const result = await authenticateWallet(address, nonce, signature);\n\n if (!result.success) {\n res.status(401).json({ error: result.reason });\n return;\n }\n\n res.status(200).json({\n accessToken: result.accessToken,\n refreshToken: result.refreshToken,\n merchant: result.merchant,\n });\n } catch (error) {\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n\nexport const verifyEmailController = async (req: Request, res: Response): Promise => {\n const merchant = req.merchant;\n\n if (!merchant) {\n res.status(401).json({ error: 'Unauthorized' });\n return;\n }\n\n const { code } = req.body;\n if (!code || typeof code !== 'string') {\n res.status(400).json({ error: 'code is required' });\n return;\n }\n\n try {\n const updatedMerchant = await verifyEmailOtp(merchant.id, code.trim());\n res.status(200).json(sanitizeMerchant(updatedMerchant));\n } catch (error) {\n if (error instanceof AppError) {\n res.status(error.statusCode).json({ error: error.message });\n return;\n }\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n\nexport const resendOtpController = async (req: Request, res: Response): Promise => {\n const merchant = req.merchant;\n\n if (!merchant) {\n res.status(401).json({ error: 'Unauthorized' });\n return;\n }\n\n try {\n await resendEmailOtp(merchant.id);\n res.status(200).json({ message: 'Verification code sent' });\n } catch (error) {\n if (error instanceof AppError) {\n res.status(error.statusCode).json({ error: error.message });\n return;\n }\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\controllers\\index.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\controllers\\invoice.controllers.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\controllers\\merchant.controllers.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 14, + "column": 77, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 14, + "endColumn": 79 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 14, + "column": 77, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 14, + "endColumn": 79 + }, + { + "ruleId": "@typescript-eslint/no-unused-vars", + "severity": 2, + "message": "'error' is defined but never used.", + "line": 18, + "column": 12, + "nodeType": null, + "messageId": "unusedVar", + "endLine": 18, + "endColumn": 17 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 23, + "column": 74, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 23, + "endColumn": 76 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 23, + "column": 74, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 23, + "endColumn": 76 + }, + { + "ruleId": "@typescript-eslint/no-unused-vars", + "severity": 2, + "message": "'error' is defined but never used.", + "line": 27, + "column": 12, + "nodeType": null, + "messageId": "unusedVar", + "endLine": 27, + "endColumn": 17 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 32, + "column": 76, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 32, + "endColumn": 78 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 32, + "column": 76, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 32, + "endColumn": 78 + }, + { + "ruleId": "@typescript-eslint/no-unused-vars", + "severity": 2, + "message": "'error' is defined but never used.", + "line": 36, + "column": 12, + "nodeType": null, + "messageId": "unusedVar", + "endLine": 36, + "endColumn": 17 + } + ], + "suppressedMessages": [], + "errorCount": 3, + "fatalErrorCount": 0, + "warningCount": 6, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import { Request, Response } from 'express';\nimport {\n createMerchant,\n getMerchant,\n listMerchants,\n registerMerchant,\n getMyProfile,\n updateMyProfile,\n generateMerchantSigningKey,\n} from '../services/merchant.services.js';\nimport { validateRegisterMerchant, validateUpdateMerchant } from '../utils/validation.js';\nimport { AppError } from '../utils/errors.js';\n\nexport const createMerchantController = async (req: Request, res: Response) => {\n try {\n const merchant = await createMerchant(req.body);\n res.status(201).json(merchant);\n } catch (error) {\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n\nexport const getMerchantController = async (req: Request, res: Response) => {\n try {\n const merchant = await getMerchant(Number(req.params.id));\n res.status(200).json(merchant);\n } catch (error) {\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n\nexport const listMerchantsController = async (req: Request, res: Response) => {\n try {\n const merchants = await listMerchants(Number(req.query.limit), Number(req.query.offset));\n res.status(200).json(merchants);\n } catch (error) {\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n\nexport const registerMerchantController = async (req: Request, res: Response): Promise => {\n const merchant = req.merchant;\n\n if (!merchant) {\n res.status(401).json({ error: 'Unauthorized' });\n return;\n }\n\n const errors = validateRegisterMerchant(req.body);\n if (Object.keys(errors).length > 0) {\n res.status(400).json({ error: 'Validation failed', errors });\n return;\n }\n\n try {\n const profile = await registerMerchant(merchant.id, req.body);\n res.status(200).json(profile);\n } catch (error) {\n if (error instanceof AppError) {\n res.status(error.statusCode).json({ error: error.message });\n return;\n }\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n\nexport const getMyProfileController = async (req: Request, res: Response): Promise => {\n const merchant = req.merchant;\n\n if (!merchant) {\n res.status(401).json({ error: 'Unauthorized' });\n return;\n }\n\n try {\n const profile = await getMyProfile(merchant.id);\n res.status(200).json(profile);\n } catch (error) {\n if (error instanceof AppError) {\n res.status(error.statusCode).json({ error: error.message });\n return;\n }\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n\nexport const generateSigningKeyController = async (req: Request, res: Response): Promise => {\n const merchant = req.merchant;\n\n if (!merchant) {\n res.status(401).json({ error: 'Unauthorized' });\n return;\n }\n\n try {\n const keys = await generateMerchantSigningKey(merchant.id);\n res.status(201).json(keys);\n } catch (error) {\n if (error instanceof AppError) {\n res.status(error.statusCode).json({ error: error.message });\n return;\n }\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n\nexport const updateMyProfileController = async (req: Request, res: Response): Promise => {\n const merchant = req.merchant;\n\n if (!merchant) {\n res.status(401).json({ error: 'Unauthorized' });\n return;\n }\n\n const errors = validateUpdateMerchant(req.body);\n if (Object.keys(errors).length > 0) {\n res.status(400).json({ error: 'Validation failed', errors });\n return;\n }\n\n try {\n const profile = await updateMyProfile(merchant.id, req.body);\n res.status(200).json(profile);\n } catch (error) {\n if (error instanceof AppError) {\n res.status(error.statusCode).json({ error: error.message });\n return;\n }\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\controllers\\pay.controllers.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\entities\\index.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\indexer\\handlers\\index.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\indexer\\poller.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/no-explicit-any", + "severity": 1, + "message": "Unexpected any. Specify a different type.", + "line": 10, + "column": 27, + "nodeType": "TSAnyKeyword", + "messageId": "unexpectedAny", + "endLine": 10, + "endColumn": 30, + "suggestions": [ + { + "messageId": "suggestUnknown", + "fix": { "range": [329, 332], "text": "unknown" }, + "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." + }, + { + "messageId": "suggestNever", + "fix": { "range": [329, 332], "text": "never" }, + "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." + } + ] + }, + { + "ruleId": "no-console", + "severity": 1, + "message": "Unexpected console statement. Only these console methods are allowed: warn, error.", + "line": 44, + "column": 7, + "nodeType": "MemberExpression", + "messageId": "limited", + "endLine": 44, + "endColumn": 18, + "suggestions": [ + { + "fix": { "range": [1371, 1438], "text": "" }, + "messageId": "removeConsole", + "data": { "propertyName": "log" }, + "desc": "Remove the console.log()." + } + ] + }, + { + "ruleId": "@typescript-eslint/no-explicit-any", + "severity": 1, + "message": "Unexpected any. Specify a different type.", + "line": 74, + "column": 27, + "nodeType": "TSAnyKeyword", + "messageId": "unexpectedAny", + "endLine": 74, + "endColumn": 30, + "suggestions": [ + { + "messageId": "suggestUnknown", + "fix": { "range": [2274, 2277], "text": "unknown" }, + "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." + }, + { + "messageId": "suggestNever", + "fix": { "range": [2274, 2277], "text": "never" }, + "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." + } + ] + }, + { + "ruleId": "no-console", + "severity": 1, + "message": "Unexpected console statement. Only these console methods are allowed: warn, error.", + "line": 81, + "column": 9, + "nodeType": "MemberExpression", + "messageId": "limited", + "endLine": 81, + "endColumn": 20, + "suggestions": [ + { + "fix": { "range": [2442, 2532], "text": "" }, + "messageId": "removeConsole", + "data": { "propertyName": "log" }, + "desc": "Remove the console.log()." + } + ] + }, + { + "ruleId": "no-console", + "severity": 1, + "message": "Unexpected console statement. Only these console methods are allowed: warn, error.", + "line": 139, + "column": 3, + "nodeType": "MemberExpression", + "messageId": "limited", + "endLine": 139, + "endColumn": 14, + "suggestions": [ + { + "fix": { "range": [4086, 4163], "text": "" }, + "messageId": "removeConsole", + "data": { "propertyName": "log" }, + "desc": "Remove the console.log()." + } + ] + } + ], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 5, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import { scValToNative } from '@stellar/stellar-sdk';\nimport prisma from '../config/prisma.js';\nimport { environment } from '../config/environment.js';\nimport { sorobanServer } from './sorobanClient.js';\nimport { dispatch } from './registry.js';\n\nlet isRunning = false;\nlet cursor: number | undefined;\n\nfunction decodeTopic(val: any): string {\n if (!val) return '';\n try {\n const native = scValToNative(val);\n if (typeof native === 'symbol') {\n return native.description ?? native.toString();\n }\n return String(native);\n } catch {\n return 'unknown_topic';\n }\n}\n\nexport async function tick(): Promise {\n try {\n const contractId = environment.stellar.contractId;\n if (!contractId || contractId.trim() === '') {\n throw new Error('STELLAR_CONTRACT_ID environment variable is unset or empty');\n }\n\n const latestLedgerResp = await sorobanServer.getLatestLedger();\n const latestLedger = latestLedgerResp.sequence;\n\n if (cursor === undefined) {\n const cursorRecord = await prisma.indexerCursor.findUnique({\n where: { contractId },\n });\n if (cursorRecord?.lastLedger != null) {\n cursor = cursorRecord.lastLedger;\n } else if (environment.stellar.indexerStartLedger != null) {\n cursor = environment.stellar.indexerStartLedger;\n } else {\n cursor = latestLedger;\n }\n console.log(`Indexer initialized with cursor at ledger ${cursor}`);\n }\n\n const currentCursor = cursor ?? latestLedger;\n cursor = currentCursor;\n\n if (currentCursor > latestLedger) {\n return;\n }\n\n const eventsResp = await sorobanServer.getEvents({\n startLedger: currentCursor,\n filters: [{ type: 'contract', contractIds: [contractId] }],\n limit: 100,\n });\n\n const events = eventsResp.events || [];\n const processedIds: { id: string; topic: string; ledger: number }[] = [];\n\n for (const event of events) {\n try {\n const existing = await prisma.indexerEvent.findUnique({\n where: { id: event.id },\n });\n if (existing) {\n continue;\n }\n\n const topicVal = event.topic && event.topic.length > 0 ? event.topic[0] : undefined;\n const decodedTopic = decodeTopic(topicVal);\n let decodedValue: any = null;\n try {\n decodedValue = event.value ? scValToNative(event.value) : null;\n } catch {\n decodedValue = null;\n }\n\n console.log(`Decoded event [${event.id}] - topic: ${decodedTopic}, value:`, decodedValue);\n\n await dispatch({\n id: event.id,\n topic: decodedTopic,\n ledger: event.ledger,\n txHash: event.txHash,\n data: decodedValue,\n });\n\n processedIds.push({\n id: event.id,\n topic: decodedTopic,\n ledger: event.ledger,\n });\n } catch (err) {\n console.error(`Error processing event ${event.id}:`, err);\n }\n }\n\n const nextCursor =\n events.length === 100 && events[events.length - 1]\n ? events[events.length - 1].ledger + 1\n : latestLedger + 1;\n\n await prisma.$transaction(async tx => {\n for (const item of processedIds) {\n await tx.indexerEvent.create({\n data: {\n id: item.id,\n topic: item.topic,\n ledger: item.ledger,\n },\n });\n }\n await tx.indexerCursor.upsert({\n where: { contractId },\n update: { lastLedger: nextCursor },\n create: { contractId, lastLedger: nextCursor },\n });\n });\n\n cursor = nextCursor;\n } catch (error) {\n console.error('Error in poller tick:', error);\n if (!environment.stellar.contractId || environment.stellar.contractId.trim() === '') {\n throw error;\n }\n }\n}\n\nexport async function startPolling(intervalMs = 6000): Promise {\n const contractId = environment.stellar.contractId;\n if (!contractId || contractId.trim() === '') {\n throw new Error('STELLAR_CONTRACT_ID environment variable is unset or empty');\n }\n if (isRunning) return;\n isRunning = true;\n console.log(`Starting Soroban indexer poller for contract ${contractId}...`);\n\n while (isRunning) {\n await tick();\n if (!isRunning) break;\n await new Promise(resolve => setTimeout(resolve, intervalMs));\n }\n}\n\nexport function stopPolling(): void {\n isRunning = false;\n}\n\nexport function getCursor(): number | undefined {\n return cursor;\n}\n\nexport function setCursor(val: number | undefined): void {\n cursor = val;\n}\n\nexport function resetPoller(): void {\n stopPolling();\n cursor = undefined;\n}\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\indexer\\registry.ts", + "messages": [ + { + "ruleId": "no-console", + "severity": 1, + "message": "Unexpected console statement. Only these console methods are allowed: warn, error.", + "line": 14, + "column": 5, + "nodeType": "MemberExpression", + "messageId": "limited", + "endLine": 14, + "endColumn": 16, + "suggestions": [ + { + "fix": { "range": [424, 499], "text": "" }, + "messageId": "removeConsole", + "data": { "propertyName": "log" }, + "desc": "Remove the console.log()." + } + ] + } + ], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 1, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import { DecodedEvent } from './types.js';\n\nexport type EventHandler = (event: DecodedEvent) => Promise | void;\n\nconst handlers = new Map();\n\nexport function registerEventHandler(topic: string, handler: EventHandler): void {\n handlers.set(topic, handler);\n}\n\nexport async function dispatch(event: DecodedEvent): Promise {\n const handler = handlers.get(event.topic);\n if (!handler) {\n console.log(`No handler registered for topic \"${event.topic}\", skipping.`);\n return;\n }\n await handler(event);\n}\n\nexport function clearHandlers(): void {\n handlers.clear();\n}\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\indexer\\run.ts", + "messages": [ + { + "ruleId": "no-console", + "severity": 1, + "message": "Unexpected console statement. Only these console methods are allowed: warn, error.", + "line": 4, + "column": 3, + "nodeType": "MemberExpression", + "messageId": "limited", + "endLine": 4, + "endColumn": 14, + "suggestions": [ + { + "fix": { "range": [89, 146], "text": "" }, + "messageId": "removeConsole", + "data": { "propertyName": "log" }, + "desc": "Remove the console.log()." + } + ] + }, + { + "ruleId": "no-console", + "severity": 1, + "message": "Unexpected console statement. Only these console methods are allowed: warn, error.", + "line": 9, + "column": 3, + "nodeType": "MemberExpression", + "messageId": "limited", + "endLine": 9, + "endColumn": 14, + "suggestions": [ + { + "fix": { "range": [201, 259], "text": "" }, + "messageId": "removeConsole", + "data": { "propertyName": "log" }, + "desc": "Remove the console.log()." + } + ] + } + ], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 2, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import { startPolling, stopPolling } from './poller.js';\n\nprocess.on('SIGINT', () => {\n console.log('Received SIGINT, shutting down indexer...');\n stopPolling();\n});\n\nprocess.on('SIGTERM', () => {\n console.log('Received SIGTERM, shutting down indexer...');\n stopPolling();\n});\n\nstartPolling().catch(error => {\n console.error('Fatal error starting Soroban indexer:', error);\n process.exit(1);\n});\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\indexer\\sorobanClient.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\indexer\\types.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/no-explicit-any", + "severity": 1, + "message": "Unexpected any. Specify a different type.", + "line": 6, + "column": 9, + "nodeType": "TSAnyKeyword", + "messageId": "unexpectedAny", + "endLine": 6, + "endColumn": 12, + "suggestions": [ + { + "messageId": "suggestUnknown", + "fix": { "range": [107, 110], "text": "unknown" }, + "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." + }, + { + "messageId": "suggestNever", + "fix": { "range": [107, 110], "text": "never" }, + "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." + } + ] + } + ], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 1, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "export interface DecodedEvent {\n id: string;\n topic: string;\n ledger: number;\n txHash: string;\n data: any;\n}\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\middlewares\\auth.middleware.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 19, + "column": 56, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 19, + "endColumn": 58 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 32, + "column": 47, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 32, + "endColumn": 49 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 45, + "column": 56, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 45, + "endColumn": 58 + } + ], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 3, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import { Request, Response, NextFunction } from 'express';\nimport jwt from 'jsonwebtoken';\nimport prisma from '../config/prisma.js';\nimport { environment } from '../config/environment.js';\nimport { authenticateApiKey } from '../services/api-key.services.js';\nimport { isApiKeyToken } from '../utils/api-key.utils.js';\n\nconst extractBearerToken = (req: Request): string | null => {\n const authHeader = req.headers.authorization;\n\n if (!authHeader || !authHeader.startsWith('Bearer ')) {\n return null;\n }\n\n const token = authHeader.slice('Bearer '.length).trim();\n return token || null;\n};\n\nconst authenticateRefreshToken = async (token: string) => {\n const session = await prisma.refreshToken.findUnique({\n where: { token },\n include: { merchant: true },\n });\n\n if (!session || session.expiresAt.getTime() < Date.now()) {\n return null;\n }\n\n return session.merchant;\n};\n\nconst authenticateJwt = async (token: string) => {\n try {\n const payload = jwt.verify(token, environment.jwtSecret) as { sub?: string };\n if (!payload.sub) {\n return null;\n }\n\n return prisma.merchant.findUnique({ where: { id: payload.sub } });\n } catch {\n return null;\n }\n};\n\nconst resolveMerchantFromToken = async (token: string) => {\n if (isApiKeyToken(token)) {\n return authenticateApiKey(token);\n }\n\n if (token.split('.').length === 3) {\n return authenticateJwt(token);\n }\n\n return authenticateRefreshToken(token);\n};\n\n/**\n * Authenticates API key bearer tokens, updates lastUsedAt, and attaches the merchant.\n */\nexport const apiKeyAuth = async (\n req: Request,\n res: Response,\n next: NextFunction,\n): Promise => {\n try {\n const token = extractBearerToken(req);\n\n if (!token) {\n res.status(401).json({ error: 'Authentication required' });\n return;\n }\n\n if (!isApiKeyToken(token)) {\n res.status(401).json({ error: 'Invalid or expired token' });\n return;\n }\n\n const merchant = await authenticateApiKey(token);\n if (!merchant) {\n res.status(401).json({ error: 'Invalid or expired token' });\n return;\n }\n\n req.merchant = merchant;\n next();\n } catch {\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n\n/**\n * Authenticates a merchant using refresh tokens or JWT access tokens only.\n * API keys are rejected to prevent key-management operations via API keys.\n */\nexport const authenticateSessionOnly = async (\n req: Request,\n res: Response,\n next: NextFunction,\n): Promise => {\n try {\n const token = extractBearerToken(req);\n\n if (!token) {\n res.status(401).json({ error: 'Authentication required' });\n return;\n }\n\n if (isApiKeyToken(token)) {\n res.status(401).json({ error: 'Invalid or expired token' });\n return;\n }\n\n const merchant =\n token.split('.').length === 3\n ? await authenticateJwt(token)\n : await authenticateRefreshToken(token);\n\n if (!merchant) {\n res.status(401).json({ error: 'Invalid or expired token' });\n return;\n }\n\n req.merchant = merchant;\n next();\n } catch {\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n\n/**\n * Authenticates a merchant from a bearer token.\n *\n * Accepts JWT access tokens (signed with `JWT_SECRET`), refresh session tokens,\n * or API keys. The resolved Merchant is attached to `req.merchant` on success.\n *\n * Responds with 401 when the `Authorization: Bearer ` header is missing\n * or malformed (`Authentication required`), or when the token is invalid,\n * expired, or references a merchant that no longer exists\n * (`Invalid or expired token`).\n */\nexport const authenticateMerchant = async (\n req: Request,\n res: Response,\n next: NextFunction,\n): Promise => {\n try {\n const token = extractBearerToken(req);\n\n if (!token) {\n res.status(401).json({ error: 'Authentication required' });\n return;\n }\n\n const merchant = await resolveMerchantFromToken(token);\n if (!merchant) {\n res.status(401).json({ error: 'Invalid or expired token' });\n return;\n }\n\n req.merchant = merchant;\n next();\n } catch {\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\routes\\auth.routes.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\routes\\index.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\routes\\invoice.routes.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\routes\\merchant.routes.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\routes\\pay.routes.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\server.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 4, + "column": 30, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 4, + "endColumn": 32 + }, + { + "ruleId": "no-console", + "severity": 1, + "message": "Unexpected console statement. Only these console methods are allowed: warn, error.", + "line": 8, + "column": 7, + "nodeType": "MemberExpression", + "messageId": "limited", + "endLine": 8, + "endColumn": 18, + "suggestions": [ + { + "fix": { "range": [201, 290], "text": "" }, + "messageId": "removeConsole", + "data": { "propertyName": "log" }, + "desc": "Remove the console.log()." + } + ] + } + ], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 2, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import app from './app.js';\nimport { environment } from './config/environment.js';\n\nconst startServer = async () => {\n try {\n // Start Express server\n app.listen(environment.port, () => {\n console.log(`Server running on port ${environment.port} in ${environment.nodeEnv} mode`);\n });\n } catch (error) {\n console.error('Error starting server:', error);\n process.exit(1);\n }\n};\n\nstartServer();\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\services\\api-key.services.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 34, + "column": 48, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 34, + "endColumn": 50 + } + ], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 1, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import { Merchant } from '@prisma/client';\nimport prisma from '../config/prisma.js';\nimport { AppError } from '../utils/errors.js';\nimport { generateApiKeyMaterial, hashApiKey, MAX_ACTIVE_API_KEYS } from '../utils/api-key.utils.js';\n\nexport type ApiKeySummary = {\n id: string;\n prefix: string;\n label: string | null;\n lastUsedAt: Date | null;\n createdAt: Date;\n};\n\nexport type CreateApiKeyResult = ApiKeySummary & {\n key: string;\n};\n\ntype ApiKeyListRow = {\n id: string;\n prefix: string | null;\n name: string | null;\n lastUsedAt: Date | null;\n createdAt: Date;\n};\n\nconst toApiKeySummary = (apiKey: ApiKeyListRow): ApiKeySummary => ({\n id: apiKey.id,\n prefix: apiKey.prefix ?? '',\n label: apiKey.name,\n lastUsedAt: apiKey.lastUsedAt,\n createdAt: apiKey.createdAt,\n});\n\nconst activeApiKeyWhere = (merchantId: string) => ({\n merchantId,\n revokedAt: null,\n OR: [{ expiresAt: null }, { expiresAt: { gt: new Date() } }],\n});\n\nexport const createApiKey = async (\n merchantId: string,\n label?: string,\n): Promise => {\n const { rawKey, prefix, keyHash } = generateApiKeyMaterial();\n const normalizedLabel = label?.trim() || null;\n\n const apiKey = await prisma.$transaction(async tx => {\n const activeKeys = await tx.apiKey.count({\n where: activeApiKeyWhere(merchantId),\n });\n\n if (activeKeys >= MAX_ACTIVE_API_KEYS) {\n throw new AppError(400, `Maximum of ${MAX_ACTIVE_API_KEYS} active API keys allowed`);\n }\n\n return tx.apiKey.create({\n data: {\n merchantId,\n keyHash,\n prefix,\n name: normalizedLabel,\n },\n });\n });\n\n return {\n ...toApiKeySummary(apiKey),\n key: rawKey,\n };\n};\n\nexport const listApiKeys = async (merchantId: string): Promise => {\n const apiKeys = await prisma.apiKey.findMany({\n where: {\n merchantId,\n revokedAt: null,\n },\n orderBy: { createdAt: 'desc' },\n select: {\n id: true,\n prefix: true,\n name: true,\n lastUsedAt: true,\n createdAt: true,\n },\n });\n\n return apiKeys.map(toApiKeySummary);\n};\n\nexport const revokeApiKey = async (merchantId: string, keyId: string): Promise => {\n const apiKey = await prisma.apiKey.findFirst({\n where: { id: keyId, merchantId },\n });\n\n if (!apiKey) {\n throw new AppError(404, 'API key not found');\n }\n\n if (apiKey.revokedAt) {\n throw new AppError(400, 'API key already revoked');\n }\n\n await prisma.apiKey.update({\n where: { id: keyId },\n data: { revokedAt: new Date() },\n });\n};\n\nexport const authenticateApiKey = async (rawKey: string): Promise => {\n const keyHash = hashApiKey(rawKey);\n const apiKey = await prisma.apiKey.findUnique({\n where: { keyHash },\n include: { merchant: true },\n });\n\n if (!apiKey || apiKey.revokedAt) {\n return null;\n }\n\n if (apiKey.expiresAt && apiKey.expiresAt.getTime() < Date.now()) {\n return null;\n }\n\n await prisma.apiKey.update({\n where: { id: apiKey.id },\n data: { lastUsedAt: new Date() },\n });\n\n return apiKey.merchant;\n};\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\services\\auth.services.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 19, + "column": 8, + "nodeType": "FunctionDeclaration", + "messageId": "missingReturnType", + "endLine": 19, + "endColumn": 34 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 19, + "column": 8, + "nodeType": "FunctionDeclaration", + "messageId": "missingReturnType", + "endLine": 19, + "endColumn": 34 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 32, + "column": 8, + "nodeType": "FunctionDeclaration", + "messageId": "missingReturnType", + "endLine": 32, + "endColumn": 38 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 32, + "column": 8, + "nodeType": "FunctionDeclaration", + "messageId": "missingReturnType", + "endLine": 32, + "endColumn": 38 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 71, + "column": 8, + "nodeType": "FunctionDeclaration", + "messageId": "missingReturnType", + "endLine": 71, + "endColumn": 37 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 71, + "column": 8, + "nodeType": "FunctionDeclaration", + "messageId": "missingReturnType", + "endLine": 71, + "endColumn": 37 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 98, + "column": 8, + "nodeType": "FunctionDeclaration", + "messageId": "missingReturnType", + "endLine": 98, + "endColumn": 41 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 98, + "column": 8, + "nodeType": "FunctionDeclaration", + "messageId": "missingReturnType", + "endLine": 98, + "endColumn": 41 + } + ], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 8, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import crypto from 'node:crypto';\nimport jwt from 'jsonwebtoken';\nimport { Keypair } from '@stellar/stellar-sdk';\nimport prisma from '../config/prisma.js';\nimport { environment } from '../config/environment.js';\n\nconst NONCE_EXPIRY_MS = 5 * 60 * 1000;\nconst REFRESH_TOKEN_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000;\n\nexport function buildChallengeMessage(address: string, nonce: string, createdAt: Date): string {\n return [\n 'Shade Authentication',\n `Address: ${address}`,\n `Nonce: ${nonce}`,\n `Timestamp: ${createdAt.toISOString()}`,\n ].join('\\n');\n}\n\nexport async function createNonce(address: string) {\n const nonce = crypto.randomUUID();\n const createdAt = new Date();\n const expiresAt = new Date(createdAt.getTime() + NONCE_EXPIRY_MS);\n const message = buildChallengeMessage(address, nonce, createdAt);\n\n const authNonce = await prisma.authNonce.create({\n data: { address, nonce, message, expiresAt },\n });\n\n return { nonce: authNonce.nonce, message: authNonce.message, expiresAt: authNonce.expiresAt };\n}\n\nexport async function verifySignature(address: string, nonce: string, rawSignature: string) {\n const authNonce = await prisma.authNonce.findUnique({ where: { nonce } });\n if (!authNonce) {\n return { valid: false, reason: 'Nonce not found' } as const;\n }\n if (authNonce.address !== address) {\n return { valid: false, reason: 'Address mismatch' } as const;\n }\n if (authNonce.usedAt) {\n return { valid: false, reason: 'Nonce already used' } as const;\n }\n if (new Date() > authNonce.expiresAt) {\n return { valid: false, reason: 'Nonce expired' } as const;\n }\n\n const message = buildChallengeMessage(address, authNonce.nonce, authNonce.createdAt);\n const messageBytes = Buffer.from(message, 'utf-8');\n const signatureBytes = Buffer.from(rawSignature, 'hex');\n\n let isValid: boolean;\n try {\n const keypair = Keypair.fromPublicKey(address);\n isValid = keypair.verify(messageBytes, signatureBytes);\n } catch {\n return { valid: false, reason: 'Invalid address or signature format' } as const;\n }\n\n if (!isValid) {\n return { valid: false, reason: 'Signature verification failed' } as const;\n }\n\n await prisma.authNonce.update({\n where: { id: authNonce.id },\n data: { usedAt: new Date() },\n });\n\n return { valid: true, reason: null } as const;\n}\n\nexport async function upsertMerchant(address: string) {\n const existing = await prisma.merchant.findFirst({ where: { address } });\n if (existing) {\n return existing;\n }\n const merchantId = crypto.randomInt(100_000, 999_999);\n const merchant = await prisma.merchant.create({\n data: { merchantId, address },\n });\n return merchant;\n}\n\nexport function issueAccessToken(merchantId: string, address: string): string {\n return jwt.sign({ sub: merchantId, address }, environment.jwtSecret, { expiresIn: '15m' });\n}\n\nexport async function issueRefreshToken(merchantId: string): Promise {\n const token = crypto.randomUUID();\n const expiresAt = new Date(Date.now() + REFRESH_TOKEN_EXPIRY_MS);\n\n await prisma.refreshToken.create({\n data: { merchantId, token, expiresAt },\n });\n\n return token;\n}\n\nexport async function authenticateWallet(address: string, nonce: string, signature: string) {\n const verification = await verifySignature(address, nonce, signature);\n if (!verification.valid) {\n return { success: false, reason: verification.reason } as const;\n }\n\n const merchant = await upsertMerchant(address);\n const accessToken = issueAccessToken(merchant.id, merchant.address);\n const refreshToken = await issueRefreshToken(merchant.id);\n\n return {\n success: true,\n accessToken,\n refreshToken,\n merchant: {\n id: merchant.id,\n address: merchant.address,\n isRegistered: merchant.registered,\n },\n } as const;\n}\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\services\\email.service.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 20, + "column": 64, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 20, + "endColumn": 66 + }, + { + "ruleId": "no-console", + "severity": 1, + "message": "Unexpected console statement. Only these console methods are allowed: warn, error.", + "line": 96, + "column": 7, + "nodeType": "MemberExpression", + "messageId": "limited", + "endLine": 96, + "endColumn": 18, + "suggestions": [ + { + "fix": { "range": [2612, 2690], "text": "" }, + "messageId": "removeConsole", + "data": { "propertyName": "log" }, + "desc": "Remove the console.log()." + } + ] + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 100, + "column": 73, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 100, + "endColumn": 75 + }, + { + "ruleId": "no-console", + "severity": 1, + "message": "Unexpected console statement. Only these console methods are allowed: warn, error.", + "line": 142, + "column": 7, + "nodeType": "MemberExpression", + "messageId": "limited", + "endLine": 142, + "endColumn": 18, + "suggestions": [ + { + "fix": { "range": [4583, 4675], "text": "" }, + "messageId": "removeConsole", + "data": { "propertyName": "log" }, + "desc": "Remove the console.log()." + } + ] + } + ], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 4, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import nodemailer from 'nodemailer';\nimport { Resend } from 'resend';\nimport { environment } from '../config/environment.js';\nimport type { Invoice, Merchant } from '@prisma/client';\nimport { generateInvoicePdf } from './invoice-pdf.services.js';\n\nexport interface EmailAttachment {\n filename: string;\n content: Buffer;\n}\n\nconst escapeHtml = (value: string): string =>\n value\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n\nconst buildOtpEmailContent = (firstName: string, code: string) => {\n const safeFirstName = escapeHtml(firstName);\n const subject = 'Verify your Shade email';\n const html = `\n

Hi ${safeFirstName},

\n

Your email verification code is:

\n

${code}

\n

This code expires in 10 minutes.

\n `.trim();\n const text = `Hi ${firstName},\\n\\nYour verification code is: ${code}\\n\\nThis code expires in 10 minutes.`;\n\n return { subject, html, text };\n};\n\nconst sendViaResend = async (\n to: string,\n subject: string,\n html: string,\n attachments?: EmailAttachment[],\n): Promise => {\n const resend = new Resend(environment.email.resendApiKey);\n const { error } = await resend.emails.send({\n from: environment.email.from,\n to,\n subject,\n html,\n attachments: attachments?.map(({ filename, content }) => ({ filename, content })),\n });\n\n if (error) {\n throw new Error(`Failed to send email via Resend: ${error.message}`);\n }\n};\n\nconst sendViaSmtp = async (\n to: string,\n subject: string,\n html: string,\n text: string,\n attachments?: EmailAttachment[],\n): Promise => {\n const transporter = nodemailer.createTransport({\n host: environment.email.smtp.host,\n port: environment.email.smtp.port,\n secure: environment.email.smtp.secure,\n auth: {\n user: environment.email.smtp.user,\n pass: environment.email.smtp.pass,\n },\n });\n\n await transporter.sendMail({\n from: environment.email.from,\n to,\n subject,\n html,\n text,\n attachments,\n });\n};\n\n/**\n * Delivers a one-time verification code to the merchant's email address.\n */\nexport const sendOtp = async (to: string, code: string, firstName: string): Promise => {\n const { subject, html, text } = buildOtpEmailContent(firstName, code);\n\n switch (environment.email.provider) {\n case 'resend':\n await sendViaResend(to, subject, html);\n return;\n case 'smtp':\n await sendViaSmtp(to, subject, html, text);\n return;\n case 'console':\n default:\n console.log(`[OTP] Verification code ${code} sent to ${to} for ${firstName}`);\n }\n};\n\nconst buildInvoiceEmailContent = (invoice: Invoice, merchant: Merchant) => {\n const merchantName = escapeHtml(merchant.businessName || 'Your merchant');\n const description = escapeHtml(invoice.description);\n const subject = `Invoice from ${merchant.businessName || 'Shade'}: ${invoice.description}`;\n const html = `\n

Hi,

\n

${merchantName} has sent you an invoice for ${description}.

\n

Amount: ${invoice.amount.toString()} ${escapeHtml(invoice.token)}

\n

Status: ${invoice.status}

\n

Your invoice is attached as a PDF.

\n `.trim();\n const text = `Hi,\\n\\n${merchant.businessName || 'Your merchant'} has sent you an invoice for ${invoice.description}.\\n\\nAmount: ${invoice.amount.toString()} ${invoice.token}\\nStatus: ${invoice.status}\\n\\nYour invoice is attached as a PDF.`;\n\n return { subject, html, text };\n};\n\n/**\n * Emails the invoice to `invoice.email` with a freshly generated PDF attached.\n * No-ops (does not throw) when the invoice has no email on file — callers\n * that need to surface that as a user-facing error (e.g. the /send route)\n * should check `invoice.email` before calling this.\n */\nexport const sendInvoiceEmail = async (invoice: Invoice, merchant: Merchant): Promise => {\n if (!invoice.email) {\n return;\n }\n\n const pdf = await generateInvoicePdf(invoice, merchant);\n const { subject, html, text } = buildInvoiceEmailContent(invoice, merchant);\n const attachments: EmailAttachment[] = [\n { filename: `invoice-${invoice.paymentSlug}.pdf`, content: pdf },\n ];\n\n switch (environment.email.provider) {\n case 'resend':\n await sendViaResend(invoice.email, subject, html, attachments);\n return;\n case 'smtp':\n await sendViaSmtp(invoice.email, subject, html, text, attachments);\n return;\n case 'console':\n default:\n console.log(`[Invoice email] Invoice ${invoice.paymentSlug} (${pdf.length} byte PDF) sent`);\n }\n};\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\services\\index.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\services\\invoice-pdf.services.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\services\\invoice.services.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 28, + "column": 51, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 28, + "endColumn": 53 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 28, + "column": 51, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 28, + "endColumn": 53 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 49, + "column": 83, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 49, + "endColumn": 85 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 49, + "column": 83, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 49, + "endColumn": 85 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 88, + "column": 3, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 88, + "endColumn": 5 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 88, + "column": 3, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 88, + "endColumn": 5 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 125, + "column": 66, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 125, + "endColumn": 68 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 125, + "column": 66, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 125, + "endColumn": 68 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 142, + "column": 78, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 142, + "endColumn": 80 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 142, + "column": 78, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 142, + "endColumn": 80 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 155, + "column": 67, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 155, + "endColumn": 69 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 155, + "column": 67, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 155, + "endColumn": 69 + } + ], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 12, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import type { Invoice, InvoiceStatus as PrismaInvoiceStatus, Prisma } from '@prisma/client';\nimport prisma from '../config/prisma.js';\nimport { AppError } from '../utils/errors.js';\nimport { generatePaymentSlug } from '../utils/slug.js';\nimport {\n CreateInvoiceInput,\n InvoiceListFilters,\n InvoicePagination,\n parseAmount,\n} from '../utils/invoice.validation.js';\n\nconst SLUG_MAX_RETRIES = 5;\n\n// String constants matching the Prisma `Status` enum. Defined locally so this\n// module never imports a runtime value from `@prisma/client` (the generated\n// client is mocked in tests and not generated in CI).\nconst InvoiceStatus = {\n DRAFT: 'DRAFT',\n PENDING: 'PENDING',\n PAID: 'PAID',\n CANCELLED: 'CANCELLED',\n} as const satisfies Record;\n\n/**\n * Public-facing view of an invoice. `amount` is serialized to a string because\n * `BigInt` is not JSON-serializable.\n */\nexport const sanitizeInvoice = (invoice: Invoice) => ({\n id: invoice.id,\n paymentSlug: invoice.paymentSlug,\n description: invoice.description,\n amount: invoice.amount.toString(),\n token: invoice.token,\n status: invoice.status,\n merchantId: invoice.merchantId,\n email: invoice.email,\n expiresAt: invoice.expiresAt,\n datePaid: invoice.datePaid,\n createdAt: invoice.createdAt,\n updatedAt: invoice.updatedAt,\n});\n\nconst isUniqueSlugError = (error: unknown): boolean => {\n if (typeof error !== 'object' || error === null) return false;\n const { code, meta } = error as { code?: string; meta?: { target?: unknown } };\n return code === 'P2002' && Array.isArray(meta?.target) && meta.target.includes('paymentSlug');\n};\n\nexport const createInvoice = async (merchantId: string, data: CreateInvoiceInput) => {\n const amount = parseAmount(data.amount);\n if (amount === null) {\n throw new AppError(400, 'amount must be a positive integer');\n }\n\n const status: PrismaInvoiceStatus = data.isDraft ? InvoiceStatus.DRAFT : InvoiceStatus.PENDING;\n const expiresAt = data.expiresAt ? new Date(data.expiresAt) : null;\n\n for (let attempt = 0; attempt < SLUG_MAX_RETRIES; attempt++) {\n try {\n const invoice = await prisma.invoice.create({\n data: {\n merchantId,\n description: data.description.trim(),\n amount,\n token: data.token.trim(),\n email: data.payerEmail?.trim() ?? null,\n expiresAt,\n status,\n paymentSlug: generatePaymentSlug(),\n },\n });\n return sanitizeInvoice(invoice);\n } catch (error) {\n if (isUniqueSlugError(error) && attempt < SLUG_MAX_RETRIES - 1) {\n continue;\n }\n throw error;\n }\n }\n\n throw new AppError(500, 'Failed to generate a unique payment slug');\n};\n\nexport const listInvoices = async (\n merchantId: string,\n filters: InvoiceListFilters,\n pagination: InvoicePagination,\n) => {\n const where: Prisma.InvoiceWhereInput = { merchantId };\n\n if (filters.status) {\n where.status = filters.status;\n }\n\n if (filters.token) {\n where.token = filters.token;\n }\n\n if (filters.startDate || filters.endDate) {\n where.createdAt = {};\n if (filters.startDate) where.createdAt.gte = filters.startDate;\n if (filters.endDate) where.createdAt.lte = filters.endDate;\n }\n\n const [invoices, total] = await Promise.all([\n prisma.invoice.findMany({\n where,\n take: pagination.limit,\n skip: pagination.offset,\n orderBy: { createdAt: 'desc' },\n }),\n prisma.invoice.count({ where }),\n ]);\n\n return {\n data: invoices.map(sanitizeInvoice),\n pagination: {\n limit: pagination.limit,\n offset: pagination.offset,\n total,\n },\n };\n};\n\nexport const getInvoice = async (merchantId: string, id: string) => {\n const invoice = await prisma.invoice.findFirst({\n where: { id, merchantId },\n });\n\n if (!invoice) {\n throw new AppError(404, 'Invoice not found');\n }\n\n return sanitizeInvoice(invoice);\n};\n\n/**\n * Fetches the raw invoice + merchant records, scoped to the owning merchant,\n * for the PDF/email flows that need fields beyond the sanitized public view\n * (payer address, fiat breakdown, merchant logo).\n */\nexport const getInvoiceWithMerchant = async (merchantId: string, id: string) => {\n const invoice = await prisma.invoice.findFirst({\n where: { id, merchantId },\n include: { merchant: true },\n });\n\n if (!invoice) {\n throw new AppError(404, 'Invoice not found');\n }\n\n return invoice;\n};\n\nexport const voidInvoice = async (merchantId: string, id: string) => {\n const invoice = await prisma.invoice.findFirst({\n where: { id, merchantId },\n });\n\n if (!invoice) {\n throw new AppError(404, 'Invoice not found');\n }\n\n if (invoice.status !== InvoiceStatus.PENDING) {\n throw new AppError(400, 'Only pending invoices can be voided');\n }\n\n const updated = await prisma.invoice.update({\n where: { id: invoice.id },\n data: { status: InvoiceStatus.CANCELLED },\n });\n\n return sanitizeInvoice(updated);\n};\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\services\\merchant.services.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 23, + "column": 54, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 23, + "endColumn": 56 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 23, + "column": 54, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 23, + "endColumn": 56 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 45, + "column": 66, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 45, + "endColumn": 68 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 45, + "column": 66, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 45, + "endColumn": 68 + }, + { + "ruleId": "no-useless-catch", + "severity": 2, + "message": "Unnecessary try/catch wrapper.", + "line": 46, + "column": 3, + "nodeType": "TryStatement", + "messageId": "unnecessaryCatch", + "endLine": 53, + "endColumn": 4 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 56, + "column": 55, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 56, + "endColumn": 57 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 56, + "column": 55, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 56, + "endColumn": 57 + }, + { + "ruleId": "no-useless-catch", + "severity": 2, + "message": "Unnecessary try/catch wrapper.", + "line": 57, + "column": 3, + "nodeType": "TryStatement", + "messageId": "unnecessaryCatch", + "endLine": 66, + "endColumn": 4 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 69, + "column": 68, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 69, + "endColumn": 70 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 69, + "column": 68, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 69, + "endColumn": 70 + }, + { + "ruleId": "no-useless-catch", + "severity": 2, + "message": "Unnecessary try/catch wrapper.", + "line": 70, + "column": 3, + "nodeType": "TryStatement", + "messageId": "unnecessaryCatch", + "endLine": 78, + "endColumn": 4 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 88, + "column": 89, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 88, + "endColumn": 91 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 88, + "column": 89, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 88, + "endColumn": 91 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 147, + "column": 48, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 147, + "endColumn": 50 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 147, + "column": 48, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 147, + "endColumn": 50 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 168, + "column": 62, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 168, + "endColumn": 64 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 168, + "column": 62, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 168, + "endColumn": 64 + }, + { + "ruleId": "no-console", + "severity": 1, + "message": "Unexpected console statement. Only these console methods are allowed: warn, error.", + "line": 192, + "column": 3, + "nodeType": "MemberExpression", + "messageId": "limited", + "endLine": 192, + "endColumn": 15, + "suggestions": [ + { + "fix": { "range": [5615, 5730], "text": "" }, + "messageId": "removeConsole", + "data": { "propertyName": "info" }, + "desc": "Remove the console.info()." + } + ] + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 206, + "column": 78, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 206, + "endColumn": 80 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 206, + "column": 78, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 206, + "endColumn": 80 + } + ], + "suppressedMessages": [], + "errorCount": 3, + "fatalErrorCount": 0, + "warningCount": 17, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import { Merchant, Prisma } from '@prisma/client';\nimport prisma from '../config/prisma.js';\nimport { AppError } from '../utils/errors.js';\nimport { RegisterMerchantInput, UpdateMerchantInput } from '../utils/validation.js';\nimport { generateOtp, hashOtp } from './otp.services.js';\nimport { sendOtp } from './email.service.js';\nimport { Keypair } from '@stellar/stellar-sdk';\n\nconst OTP_EXPIRY_MS = 10 * 60 * 1000;\n\ninterface MerchantData {\n merchantId: number;\n email?: string;\n address: string;\n active?: boolean;\n verified?: boolean;\n}\n\n/**\n * Returns a public-facing view of a merchant. Built as an allow-list so that\n * any sensitive fields added to the model later are never exposed by default.\n */\nexport const sanitizeMerchant = (merchant: Merchant) => ({\n id: merchant.id,\n merchantId: merchant.merchantId,\n email: merchant.email,\n address: merchant.address,\n account: merchant.account,\n merchantKey: merchant.merchantKey,\n firstName: merchant.firstName,\n lastName: merchant.lastName,\n businessName: merchant.businessName,\n category: merchant.category,\n description: merchant.description,\n logo: merchant.logo,\n webhook: merchant.webhook,\n active: merchant.active,\n verified: merchant.verified,\n emailVerified: merchant.emailVerified,\n registered: merchant.registered,\n createdAt: merchant.createdAt,\n updatedAt: merchant.updatedAt,\n});\n\nexport const createMerchant = async (merchantData: MerchantData) => {\n try {\n const merchant = await prisma.merchant.create({\n data: merchantData,\n });\n return merchant;\n } catch (error) {\n throw error;\n }\n};\n\nexport const getMerchant = async (merchantId: number) => {\n try {\n const merchant = await prisma.merchant.findUnique({\n where: {\n merchantId: merchantId,\n },\n });\n return merchant;\n } catch (error) {\n throw error;\n }\n};\n\nexport const listMerchants = async (limit: number, offset: number) => {\n try {\n const merchants = await prisma.merchant.findMany({\n take: limit,\n skip: offset,\n });\n return merchants;\n } catch (error) {\n throw error;\n }\n};\n\n/**\n * Completes a merchant's profile after wallet authentication.\n *\n * Enforces that the email is unique across merchants and that the profile has\n * not already been completed, persists the profile data, resets email\n * verification, and triggers an OTP email.\n */\nexport const registerMerchant = async (merchantId: string, data: RegisterMerchantInput) => {\n const merchant = await prisma.merchant.findUnique({\n where: { id: merchantId },\n });\n\n if (!merchant) {\n throw new AppError(404, 'Merchant not found');\n }\n\n if (merchant.registered) {\n throw new AppError(409, 'Profile already set up');\n }\n\n const normalizedEmail = data.email.trim().toLowerCase();\n\n const existingEmail = await prisma.merchant.findFirst({\n where: {\n email: normalizedEmail,\n NOT: { id: merchantId },\n },\n });\n\n if (existingEmail) {\n throw new AppError(409, 'Email already registered');\n }\n\n const code = generateOtp();\n const emailOtp = await hashOtp(code);\n const emailOtpExpiresAt = new Date(Date.now() + OTP_EXPIRY_MS);\n\n const updatedMerchant = await prisma.merchant.update({\n where: { id: merchantId },\n data: {\n firstName: data.firstName.trim(),\n lastName: data.lastName.trim(),\n email: normalizedEmail,\n businessName: data.businessName.trim(),\n category: data.category.trim(),\n description: data.description.trim(),\n logo: data.logo?.trim() ?? null,\n emailVerified: false,\n registered: true,\n emailOtp,\n emailOtpExpiresAt,\n },\n });\n\n try {\n await sendOtp(normalizedEmail, code, data.firstName.trim());\n } catch (err) {\n console.error('Failed to send OTP email after registration', err);\n }\n\n return sanitizeMerchant(updatedMerchant);\n};\n\n/**\n * Returns the authenticated merchant's own profile.\n */\nexport const getMyProfile = async (id: string) => {\n const merchant = await prisma.merchant.findUnique({ where: { id } });\n\n if (!merchant) {\n throw new AppError(404, 'Merchant not found');\n }\n\n return sanitizeMerchant(merchant);\n};\n\n/**\n * Generates a fresh Ed25519 signing keypair for the merchant.\n *\n * Persists ONLY the hex-encoded 32-byte public key to `Merchant.merchantKey`,\n * overwriting any previous value (unconditional generate-and-replace). Returns\n * both halves; the hex-encoded 32-byte private key is returned exactly once and\n * is never written to the database or logged.\n *\n * Uploading the public key on-chain (`set_merchant_key`) and signing invoices\n * with the private key are done client/SDK-side and are out of scope here.\n */\nexport const generateMerchantSigningKey = async (id: string) => {\n const merchant = await prisma.merchant.findUnique({ where: { id } });\n\n if (!merchant) {\n throw new AppError(404, 'Merchant not found');\n }\n\n const keypair = Keypair.random();\n const publicKey = Buffer.from(keypair.rawPublicKey()).toString('hex');\n const privateKey = Buffer.from(keypair.rawSecretKey()).toString('hex');\n\n // Optimistic concurrency: only replace the key we just read. If a concurrent\n // rotation already changed it, no row matches and we reject rather than return\n // a private key whose public half is no longer the one persisted.\n const { count } = await prisma.merchant.updateMany({\n where: { id, merchantKey: merchant.merchantKey },\n data: { merchantKey: publicKey },\n });\n\n if (count !== 1) {\n throw new AppError(409, 'Signing key was changed concurrently; please retry');\n }\n\n // Audit only — never include the private key here.\n console.info(\n `[merchant] signing key ${merchant.merchantKey ? 'rotated' : 'created'} for merchant ${id}`,\n );\n\n return { publicKey, privateKey };\n};\n\n/**\n * Partially updates the authenticated merchant's editable profile fields.\n *\n * Only fields present in `data` are written. Strings are trimmed; an empty\n * `logo`/`webhook` is normalized to null so the merchant can clear them.\n * Non-editable fields are never read here, so they cannot be changed.\n */\nexport const updateMyProfile = async (id: string, data: UpdateMerchantInput) => {\n const updateData: Prisma.MerchantUpdateInput = {};\n\n const textFields = ['firstName', 'lastName', 'businessName', 'category', 'description'] as const;\n for (const field of textFields) {\n const value = data[field];\n if (value !== undefined) {\n updateData[field] = value.trim();\n }\n }\n\n if (data.logo !== undefined) {\n const logo = typeof data.logo === 'string' ? data.logo.trim() : data.logo;\n updateData.logo = logo ? logo : null;\n }\n\n if (data.webhook !== undefined) {\n const webhook = typeof data.webhook === 'string' ? data.webhook.trim() : data.webhook;\n updateData.webhook = webhook ? webhook : null;\n }\n\n const updated = await prisma.merchant.update({ where: { id }, data: updateData });\n\n return sanitizeMerchant(updated);\n};\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\services\\otp.services.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 49, + "column": 72, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 49, + "endColumn": 74 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 49, + "column": 72, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 49, + "endColumn": 74 + } + ], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 2, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import { randomInt } from 'node:crypto';\nimport bcrypt from 'bcrypt';\nimport prisma from '../config/prisma.js';\nimport { AppError } from '../utils/errors.js';\nimport { sendOtp } from './email.service.js';\n\nconst OTP_LENGTH = 6;\nconst OTP_EXPIRY_MS = 10 * 60 * 1000;\nconst OTP_RESEND_COOLDOWN_MS = 60 * 1000;\nconst BCRYPT_ROUNDS = 10;\n\nexport const generateOtp = (): string => {\n const min = 10 ** (OTP_LENGTH - 1);\n const max = 10 ** OTP_LENGTH - 1;\n return randomInt(min, max + 1).toString();\n};\n\nexport const hashOtp = async (code: string): Promise => bcrypt.hash(code, BCRYPT_ROUNDS);\n\nexport const verifyOtpHash = async (code: string, hash: string): Promise =>\n bcrypt.compare(code, hash);\n\nconst getLastOtpSentAt = (expiresAt: Date): Date => new Date(expiresAt.getTime() - OTP_EXPIRY_MS);\n\n/**\n * Generates a 6-digit OTP, stores its bcrypt hash with a 10-minute expiry,\n * and sends the code to the merchant's email.\n */\nexport const issueEmailOtp = async (merchant: {\n id: string;\n email: string;\n firstName: string | null;\n}): Promise => {\n const code = generateOtp();\n const emailOtp = await hashOtp(code);\n const emailOtpExpiresAt = new Date(Date.now() + OTP_EXPIRY_MS);\n\n await prisma.merchant.update({\n where: { id: merchant.id },\n data: { emailOtp, emailOtpExpiresAt },\n });\n\n await sendOtp(merchant.email, code, merchant.firstName?.trim() || 'there');\n};\n\n/**\n * Validates the submitted OTP against the stored hash and marks the email verified.\n */\nexport const verifyEmailOtp = async (merchantId: string, code: string) => {\n const merchant = await prisma.merchant.findUnique({\n where: { id: merchantId },\n });\n\n if (!merchant?.emailOtp || !merchant.emailOtpExpiresAt) {\n throw new AppError(400, 'Invalid verification code');\n }\n\n if (merchant.emailOtpExpiresAt.getTime() < Date.now()) {\n throw new AppError(400, 'Code expired');\n }\n\n const isValid = await verifyOtpHash(code, merchant.emailOtp);\n if (!isValid) {\n throw new AppError(400, 'Invalid verification code');\n }\n\n return prisma.merchant.update({\n where: { id: merchantId },\n data: {\n emailVerified: true,\n emailOtp: null,\n emailOtpExpiresAt: null,\n },\n });\n};\n\n/**\n * Re-generates and re-sends the email OTP, rate-limited to one request per minute.\n */\nexport const resendEmailOtp = async (merchantId: string): Promise => {\n const merchant = await prisma.merchant.findUnique({\n where: { id: merchantId },\n });\n\n if (!merchant) {\n throw new AppError(404, 'Merchant not found');\n }\n\n if (!merchant.registered || !merchant.email) {\n throw new AppError(400, 'Registration incomplete');\n }\n\n if (merchant.emailVerified) {\n throw new AppError(400, 'Email already verified');\n }\n\n if (merchant.emailOtpExpiresAt) {\n const lastSentAt = getLastOtpSentAt(merchant.emailOtpExpiresAt);\n if (Date.now() - lastSentAt.getTime() < OTP_RESEND_COOLDOWN_MS) {\n throw new AppError(429, 'Please wait before requesting a new code');\n }\n }\n\n await issueEmailOtp({\n id: merchant.id,\n email: merchant.email,\n firstName: merchant.firstName,\n });\n};\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\services\\pay.services.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 13, + "column": 97, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 13, + "endColumn": 99 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 27, + "column": 58, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 27, + "endColumn": 60 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 27, + "column": 58, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 27, + "endColumn": 60 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 70, + "column": 60, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 70, + "endColumn": 62 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 70, + "column": 60, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 70, + "endColumn": 62 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 85, + "column": 91, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 85, + "endColumn": 93 + }, + { + "ruleId": "@typescript-eslint/explicit-module-boundary-types", + "severity": 1, + "message": "Missing return type on function.", + "line": 85, + "column": 91, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 85, + "endColumn": 93 + } + ], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 7, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import prisma from '../config/prisma.js';\nimport { AppError } from '../utils/errors.js';\nimport type { InvoiceStatus as PrismaInvoiceStatus } from '@prisma/client';\n\nconst InvoiceStatus = {\n DRAFT: 'DRAFT',\n PENDING: 'PENDING',\n PAID: 'PAID',\n CANCELLED: 'CANCELLED',\n REFUNDED: 'REFUNDED',\n} as const satisfies Record;\n\nconst assertInvoiceVisible = (invoice: { status: PrismaInvoiceStatus; expiresAt: Date | null }) => {\n if (\n invoice.status === InvoiceStatus.CANCELLED ||\n invoice.status === InvoiceStatus.PAID ||\n invoice.status === InvoiceStatus.REFUNDED\n ) {\n throw new AppError(410, 'Invoice is no longer available');\n }\n\n if (invoice.expiresAt && invoice.expiresAt < new Date()) {\n throw new AppError(410, 'expired');\n }\n};\n\nexport const resolveInvoiceBySlug = async (slug: string) => {\n const invoice = await prisma.invoice.findUnique({\n where: { paymentSlug: slug },\n select: {\n paymentSlug: true,\n description: true,\n amount: true,\n token: true,\n status: true,\n expiresAt: true,\n pricingMode: true,\n merchant: {\n select: {\n businessName: true,\n },\n },\n },\n });\n\n if (!invoice) {\n throw new AppError(404, 'Invoice not found');\n }\n\n assertInvoiceVisible(invoice);\n\n return {\n slug: invoice.paymentSlug,\n description: invoice.description,\n amount: invoice.amount.toString(),\n token: invoice.token,\n status: invoice.status,\n merchantName: invoice.merchant.businessName,\n expiresAt: invoice.expiresAt,\n pricingMode: invoice.pricingMode,\n };\n};\n\n/**\n * Fetches the full invoice + merchant records for a publicly visible invoice,\n * applying the same 404/410 visibility rules as `resolveInvoiceBySlug`. Used\n * by the public PDF download route, which needs raw fields (payer, dates,\n * fiat breakdown, logo) rather than the trimmed public-facing view.\n */\nexport const getInvoiceForPdfBySlug = async (slug: string) => {\n const invoice = await prisma.invoice.findUnique({\n where: { paymentSlug: slug },\n include: { merchant: true },\n });\n\n if (!invoice) {\n throw new AppError(404, 'Invoice not found');\n }\n\n assertInvoiceVisible(invoice);\n\n return invoice;\n};\n\nexport const confirmPayment = async (slug: string, payerAddress: string, txHash?: string) => {\n return await prisma.$transaction(async tx => {\n const invoice = await tx.invoice.findUnique({\n where: { paymentSlug: slug },\n });\n\n if (!invoice) {\n throw new AppError(404, 'Invoice not found');\n }\n\n assertInvoiceVisible(invoice);\n\n const idempotencyKey = `${invoice.id}-${payerAddress}-${txHash || 'none'}`;\n\n const confirmation = await tx.paymentConfirmation.upsert({\n where: { idempotencyKey },\n update: {},\n create: {\n invoiceId: invoice.id,\n merchantId: invoice.merchantId,\n payerAddress,\n txHash: txHash || null,\n idempotencyKey,\n },\n });\n\n return confirmation;\n });\n};\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\services\\storage\\invoice-pdf.storage.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\types\\express.d.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\utils\\api-key.utils.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\utils\\errors.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\utils\\invoice.validation.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\utils\\slug.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\utils\\validation.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\__mocks__\\prisma.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/no-unused-vars", + "severity": 2, + "message": "'DeepMockProxy' is defined but never used. Allowed unused vars must match /^_/u.", + "line": 2, + "column": 31, + "nodeType": null, + "messageId": "unusedVar", + "endLine": 2, + "endColumn": 44 + } + ], + "suppressedMessages": [], + "errorCount": 1, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import { jest, beforeEach } from '@jest/globals';\nimport { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended';\nimport { PrismaClient } from '@prisma/client';\n\nexport const prismaMock = mockDeep();\n\njest.mock('../../src/config/prisma.js', () => ({\n __esModule: true,\n default: prismaMock,\n}));\n\nbeforeEach(() => {\n mockReset(prismaMock);\n});\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\helpers\\api-key.fixtures.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\integration\\api-key.routes.test.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/no-unused-vars", + "severity": 2, + "message": "'TEST_API_KEY_PREFIX' is defined but never used. Allowed unused vars must match /^_/u.", + "line": 5, + "column": 3, + "nodeType": null, + "messageId": "unusedVar", + "endLine": 5, + "endColumn": 22 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 20, + "column": 5, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 20, + "endColumn": 20 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 21, + "column": 5, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 21, + "endColumn": 17 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 22, + "column": 5, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 22, + "endColumn": 29 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 68, + "column": 36, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 68, + "endColumn": 38 + } + ], + "suppressedMessages": [], + "errorCount": 1, + "fatalErrorCount": 0, + "warningCount": 4, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import { jest } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\nimport request from 'supertest';\nimport {\n TEST_API_KEY_PREFIX,\n TEST_KEY_HASH,\n TEST_KEY_PREFIX_DISPLAY,\n TEST_RAW_API_KEY,\n} from '../helpers/api-key.fixtures.js';\n\njest.unstable_mockModule('../../src/utils/api-key.utils.js', () => {\n const prefix = 'sk_' + 'live_';\n const rawKey = `${prefix}testkey1234567890123456789012345`;\n return {\n __esModule: true,\n API_KEY_PREFIX: prefix,\n API_KEY_RANDOM_LENGTH: 32,\n API_KEY_DISPLAY_PREFIX_LENGTH: 8,\n MAX_ACTIVE_API_KEYS: 10,\n isApiKeyToken: (token: string) => token.startsWith(prefix),\n hashApiKey: (rawKeyValue: string) => `hash-${rawKeyValue}`,\n generateApiKeyMaterial: () => ({\n rawKey,\n prefix: `${prefix}testkey1`,\n keyHash: `hash-${rawKey}`,\n }),\n };\n});\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { default: app } = await import('../../src/app.js');\n\nconst merchant = {\n id: 'merchant-1',\n merchantId: 1,\n address: '0x123',\n account: null,\n email: 'merchant@example.com',\n firstName: 'Ada',\n lastName: 'Lovelace',\n businessName: 'Engines',\n category: 'software',\n description: 'desc',\n logo: null,\n webhook: null,\n active: true,\n verified: false,\n emailVerified: true,\n registered: true,\n emailOtp: null,\n emailOtpExpiresAt: null,\n createdAt: new Date('2026-06-27T12:00:00.000Z'),\n updatedAt: new Date('2026-06-27T12:00:00.000Z'),\n};\n\nconst baseApiKey = {\n id: 'key-1',\n merchantId: merchant.id,\n keyHash: TEST_KEY_HASH,\n prefix: TEST_KEY_PREFIX_DISPLAY,\n name: 'Production',\n lastUsedAt: null,\n expiresAt: null,\n revokedAt: null,\n createdAt: new Date('2026-06-27T12:00:00.000Z'),\n};\n\nconst authenticateWithSession = () => {\n prismaMock.refreshToken.findUnique.mockResolvedValue({\n id: 'session-1',\n merchantId: merchant.id,\n token: 'valid-token',\n expiresAt: new Date(Date.now() + 60 * 60 * 1000),\n createdAt: new Date(),\n merchant,\n } as any);\n};\n\ndescribe('Merchant API key routes', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n prismaMock.$transaction.mockImplementation(\n async (callback: (tx: typeof prismaMock) => unknown) => callback(prismaMock),\n );\n });\n\n describe('POST /api/v1/merchants/api-keys', () => {\n test('returns 401 when unauthenticated', async () => {\n const response = await request(app)\n .post('/api/v1/merchants/api-keys')\n .send({ label: 'Production' });\n\n expect(response.status).toBe(401);\n });\n\n test('returns 201 with raw key only on creation', async () => {\n authenticateWithSession();\n prismaMock.apiKey.count.mockResolvedValue(0);\n prismaMock.apiKey.create.mockResolvedValue(baseApiKey as any);\n\n const response = await request(app)\n .post('/api/v1/merchants/api-keys')\n .set('Authorization', 'Bearer valid-token')\n .send({ label: 'Production' });\n\n expect(response.status).toBe(201);\n expect(response.body).toEqual({\n id: 'key-1',\n key: TEST_RAW_API_KEY,\n prefix: TEST_KEY_PREFIX_DISPLAY,\n label: 'Production',\n lastUsedAt: null,\n createdAt: baseApiKey.createdAt.toISOString(),\n });\n });\n\n test('returns 400 when label is not a string', async () => {\n authenticateWithSession();\n\n const response = await request(app)\n .post('/api/v1/merchants/api-keys')\n .set('Authorization', 'Bearer valid-token')\n .send({ label: 123 });\n\n expect(response.status).toBe(400);\n expect(response.body).toEqual({ error: 'label must be a string' });\n expect(prismaMock.apiKey.create).not.toHaveBeenCalled();\n });\n\n test('returns 401 when authenticated with an API key', async () => {\n prismaMock.apiKey.findUnique.mockResolvedValue({\n ...baseApiKey,\n merchant,\n } as any);\n\n const response = await request(app)\n .post('/api/v1/merchants/api-keys')\n .set('Authorization', `Bearer ${TEST_RAW_API_KEY}`)\n .send({ label: 'Secondary' });\n\n expect(response.status).toBe(401);\n expect(prismaMock.apiKey.create).not.toHaveBeenCalled();\n });\n\n test('returns 400 when active key limit is exceeded', async () => {\n authenticateWithSession();\n prismaMock.apiKey.count.mockResolvedValue(10);\n\n const response = await request(app)\n .post('/api/v1/merchants/api-keys')\n .set('Authorization', 'Bearer valid-token')\n .send({ label: 'Another key' });\n\n expect(response.status).toBe(400);\n expect(response.body).toEqual({ error: 'Maximum of 10 active API keys allowed' });\n });\n });\n\n describe('GET /api/v1/merchants/api-keys', () => {\n test('returns non-revoked keys without raw key or hash', async () => {\n authenticateWithSession();\n prismaMock.apiKey.findMany.mockResolvedValue([baseApiKey] as any);\n\n const response = await request(app)\n .get('/api/v1/merchants/api-keys')\n .set('Authorization', 'Bearer valid-token');\n\n expect(response.status).toBe(200);\n expect(response.body).toEqual([\n {\n id: 'key-1',\n prefix: TEST_KEY_PREFIX_DISPLAY,\n label: 'Production',\n lastUsedAt: null,\n createdAt: baseApiKey.createdAt.toISOString(),\n },\n ]);\n expect(JSON.stringify(response.body)).not.toContain('keyHash');\n expect(JSON.stringify(response.body)).not.toContain(TEST_RAW_API_KEY);\n });\n });\n\n describe('DELETE /api/v1/merchants/api-keys/:id', () => {\n test('revokes an owned key', async () => {\n authenticateWithSession();\n prismaMock.apiKey.findFirst.mockResolvedValue(baseApiKey as any);\n prismaMock.apiKey.update.mockResolvedValue({ ...baseApiKey, revokedAt: new Date() } as any);\n\n const response = await request(app)\n .delete('/api/v1/merchants/api-keys/key-1')\n .set('Authorization', 'Bearer valid-token');\n\n expect(response.status).toBe(200);\n expect(response.body).toEqual({ message: 'API key revoked' });\n });\n\n test('returns 404 when key belongs to another merchant', async () => {\n authenticateWithSession();\n prismaMock.apiKey.findFirst.mockResolvedValue(null);\n\n const response = await request(app)\n .delete('/api/v1/merchants/api-keys/key-2')\n .set('Authorization', 'Bearer valid-token');\n\n expect(response.status).toBe(404);\n expect(response.body).toEqual({ error: 'API key not found' });\n });\n });\n});\n\ndescribe('API key authentication middleware', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n });\n\n test('allows invoice access with a valid API key and updates lastUsedAt', async () => {\n prismaMock.apiKey.findUnique.mockResolvedValue({\n ...baseApiKey,\n merchant,\n } as any);\n prismaMock.apiKey.update.mockResolvedValue(baseApiKey as any);\n prismaMock.invoice.findMany.mockResolvedValue([]);\n\n const response = await request(app)\n .get('/api/v1/invoices')\n .set('Authorization', `Bearer ${TEST_RAW_API_KEY}`);\n\n expect(response.status).toBe(200);\n expect(prismaMock.apiKey.update).toHaveBeenCalledWith({\n where: { id: 'key-1' },\n data: { lastUsedAt: expect.any(Date) },\n });\n expect(prismaMock.refreshToken.findUnique).not.toHaveBeenCalled();\n });\n\n test('returns 401 for revoked API keys', async () => {\n prismaMock.apiKey.findUnique.mockResolvedValue({\n ...baseApiKey,\n revokedAt: new Date(),\n merchant,\n } as any);\n\n const response = await request(app)\n .get('/api/v1/invoices')\n .set('Authorization', `Bearer ${TEST_RAW_API_KEY}`);\n\n expect(response.status).toBe(401);\n });\n});\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\integration\\auth.email-otp.test.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 45, + "column": 60, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 45, + "endColumn": 62 + } + ], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 1, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import { jest } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\nimport request from 'supertest';\n\nconst sendOtpMock = jest.fn(async () => undefined);\n\njest.unstable_mockModule('../../src/services/email.service.js', () => ({\n __esModule: true,\n sendOtp: sendOtpMock,\n sendInvoiceEmail: jest.fn(async () => undefined),\n}));\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { default: app } = await import('../../src/app.js');\nconst bcrypt = await import('bcrypt');\n\nconst VERIFY_EMAIL_URL = '/api/v1/auth/verify-email';\nconst RESEND_OTP_URL = '/api/v1/auth/resend-otp';\n\nconst mockDate = new Date('2026-06-21T12:00:00Z');\n\nconst registeredMerchant = {\n id: 'uuid-1',\n merchantId: 1,\n address: '0x123',\n account: null,\n email: 'ada@example.com',\n firstName: 'Ada',\n lastName: 'Lovelace',\n businessName: 'Analytical Engines',\n category: 'software',\n description: 'We build computing machines.',\n logo: null,\n webhook: null,\n active: true,\n verified: false,\n emailVerified: false,\n registered: true,\n emailOtp: null as string | null,\n emailOtpExpiresAt: null as Date | null,\n createdAt: mockDate,\n updatedAt: mockDate,\n};\n\nconst authenticateAs = (merchant: Record) => {\n prismaMock.refreshToken.findUnique.mockResolvedValue({\n id: 'session-1',\n merchantId: merchant.id,\n token: 'valid-token',\n expiresAt: new Date(Date.now() + 60 * 60 * 1000),\n createdAt: mockDate,\n merchant,\n } as any);\n};\n\ndescribe('Email OTP auth routes', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n sendOtpMock.mockClear();\n jest.useFakeTimers({ now: mockDate });\n });\n\n afterEach(() => {\n jest.useRealTimers();\n });\n\n describe('POST /api/v1/auth/verify-email', () => {\n test('returns 401 for unauthenticated requests', async () => {\n const response = await request(app).post(VERIFY_EMAIL_URL).send({ code: '123456' });\n\n expect(response.status).toBe(401);\n expect(response.body).toEqual({ error: 'Authentication required' });\n });\n\n test('returns 400 when code is missing', async () => {\n authenticateAs(registeredMerchant);\n\n const response = await request(app)\n .post(VERIFY_EMAIL_URL)\n .set('Authorization', 'Bearer valid-token')\n .send({});\n\n expect(response.status).toBe(400);\n expect(response.body).toEqual({ error: 'code is required' });\n });\n\n test('returns 200 and marks emailVerified true with correct code', async () => {\n const code = '123456';\n const emailOtp = await bcrypt.hash(code, 10);\n const merchantWithOtp = {\n ...registeredMerchant,\n emailOtp,\n emailOtpExpiresAt: new Date('2026-06-21T12:05:00.000Z'),\n };\n\n authenticateAs(merchantWithOtp);\n prismaMock.merchant.findUnique.mockResolvedValue(merchantWithOtp as any);\n prismaMock.merchant.update.mockImplementation(async (args: any) => ({\n ...merchantWithOtp,\n ...args.data,\n }));\n\n const response = await request(app)\n .post(VERIFY_EMAIL_URL)\n .set('Authorization', 'Bearer valid-token')\n .send({ code });\n\n expect(response.status).toBe(200);\n expect(response.body.emailVerified).toBe(true);\n expect(prismaMock.merchant.update).toHaveBeenCalledWith({\n where: { id: 'uuid-1' },\n data: {\n emailVerified: true,\n emailOtp: null,\n emailOtpExpiresAt: null,\n },\n });\n });\n\n test('returns 400 for wrong code', async () => {\n const emailOtp = await bcrypt.hash('123456', 10);\n const merchantWithOtp = {\n ...registeredMerchant,\n emailOtp,\n emailOtpExpiresAt: new Date('2026-06-21T12:05:00.000Z'),\n };\n\n authenticateAs(merchantWithOtp);\n prismaMock.merchant.findUnique.mockResolvedValue(merchantWithOtp as any);\n\n const response = await request(app)\n .post(VERIFY_EMAIL_URL)\n .set('Authorization', 'Bearer valid-token')\n .send({ code: '654321' });\n\n expect(response.status).toBe(400);\n expect(response.body).toEqual({ error: 'Invalid verification code' });\n });\n\n test('returns 400 with Code expired for expired code', async () => {\n const code = '123456';\n const emailOtp = await bcrypt.hash(code, 10);\n const merchantWithOtp = {\n ...registeredMerchant,\n emailOtp,\n emailOtpExpiresAt: new Date('2026-06-21T11:59:00.000Z'),\n };\n\n authenticateAs(merchantWithOtp);\n prismaMock.merchant.findUnique.mockResolvedValue(merchantWithOtp as any);\n\n const response = await request(app)\n .post(VERIFY_EMAIL_URL)\n .set('Authorization', 'Bearer valid-token')\n .send({ code });\n\n expect(response.status).toBe(400);\n expect(response.body).toEqual({ error: 'Code expired' });\n });\n });\n\n describe('POST /api/v1/auth/resend-otp', () => {\n test('returns 401 for unauthenticated requests', async () => {\n const response = await request(app).post(RESEND_OTP_URL);\n\n expect(response.status).toBe(401);\n expect(response.body).toEqual({ error: 'Authentication required' });\n });\n\n test('returns 200 and re-sends OTP when cooldown has elapsed', async () => {\n const merchantWithOtp = {\n ...registeredMerchant,\n emailOtp: 'hashed',\n emailOtpExpiresAt: new Date('2026-06-21T11:58:00.000Z'),\n };\n\n authenticateAs(merchantWithOtp);\n prismaMock.merchant.findUnique.mockResolvedValue(merchantWithOtp as any);\n prismaMock.merchant.update.mockResolvedValue(merchantWithOtp as any);\n\n const response = await request(app)\n .post(RESEND_OTP_URL)\n .set('Authorization', 'Bearer valid-token');\n\n expect(response.status).toBe(200);\n expect(response.body).toEqual({ message: 'Verification code sent' });\n expect(sendOtpMock).toHaveBeenCalledWith(\n 'ada@example.com',\n expect.stringMatching(/^\\d{6}$/),\n 'Ada',\n );\n expect(prismaMock.merchant.update).toHaveBeenCalledWith({\n where: { id: 'uuid-1' },\n data: {\n emailOtp: expect.any(String),\n emailOtpExpiresAt: expect.any(Date),\n },\n });\n });\n\n test('returns 429 when resend is requested within one minute', async () => {\n const merchantWithOtp = {\n ...registeredMerchant,\n emailOtp: 'hashed',\n emailOtpExpiresAt: new Date('2026-06-21T12:09:30.000Z'),\n };\n\n authenticateAs(merchantWithOtp);\n prismaMock.merchant.findUnique.mockResolvedValue(merchantWithOtp as any);\n\n const response = await request(app)\n .post(RESEND_OTP_URL)\n .set('Authorization', 'Bearer valid-token');\n\n expect(response.status).toBe(429);\n expect(response.body).toEqual({ error: 'Please wait before requesting a new code' });\n expect(sendOtpMock).not.toHaveBeenCalled();\n });\n });\n});\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\integration\\auth.middleware.test.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/no-unused-vars", + "severity": 2, + "message": "'jest' is defined but never used. Allowed unused vars must match /^_/u.", + "line": 1, + "column": 10, + "nodeType": null, + "messageId": "unusedVar", + "endLine": 1, + "endColumn": 14 + } + ], + "suppressedMessages": [], + "errorCount": 1, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import { jest } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\nimport jwt from 'jsonwebtoken';\nimport request from 'supertest';\nimport {\n TEST_INTEGRATION_PREFIX,\n TEST_INTEGRATION_RAW_API_KEY,\n TEST_UNKNOWN_RAW_API_KEY,\n testApiKeyRegex,\n} from '../helpers/api-key.fixtures.js';\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { default: app } = await import('../../src/app.js');\nconst { environment } = await import('../../src/config/environment.js');\nconst { hashApiKey } = await import('../../src/utils/api-key.utils.js');\n\nconst merchant = {\n id: 'merchant-1',\n merchantId: 1,\n address: '0x123',\n account: null,\n email: 'merchant@example.com',\n firstName: 'Ada',\n lastName: 'Lovelace',\n businessName: 'Engines',\n category: 'software',\n description: 'desc',\n logo: null,\n webhook: null,\n active: true,\n verified: false,\n emailVerified: true,\n registered: true,\n emailOtp: null,\n emailOtpExpiresAt: null,\n createdAt: new Date('2026-06-27T12:00:00.000Z'),\n updatedAt: new Date('2026-06-27T12:00:00.000Z'),\n};\n\nconst rawApiKey = TEST_INTEGRATION_RAW_API_KEY;\nconst apiKeyRecord = {\n id: 'key-1',\n merchantId: merchant.id,\n keyHash: hashApiKey(rawApiKey),\n prefix: TEST_INTEGRATION_PREFIX,\n name: 'Integration',\n lastUsedAt: null,\n expiresAt: null,\n revokedAt: null,\n createdAt: new Date('2026-06-27T12:00:00.000Z'),\n};\n\ndescribe('authenticateMerchant auth paths', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n });\n\n test('accepts valid refresh session tokens', async () => {\n prismaMock.refreshToken.findUnique.mockResolvedValue({\n id: 'session-1',\n merchantId: merchant.id,\n token: 'valid-session-token',\n expiresAt: new Date(Date.now() + 60 * 60 * 1000),\n createdAt: new Date(),\n merchant,\n } as any);\n prismaMock.apiKey.findMany.mockResolvedValue([]);\n\n const response = await request(app)\n .get('/api/v1/merchants/api-keys')\n .set('Authorization', 'Bearer valid-session-token');\n\n expect(response.status).toBe(200);\n expect(prismaMock.refreshToken.findUnique).toHaveBeenCalled();\n expect(prismaMock.apiKey.findUnique).not.toHaveBeenCalled();\n });\n\n test('accepts valid JWT access tokens', async () => {\n const accessToken = jwt.sign(\n { sub: merchant.id, address: merchant.address },\n environment.jwtSecret,\n {\n expiresIn: '15m',\n },\n );\n prismaMock.merchant.findUnique.mockResolvedValue(merchant as any);\n prismaMock.apiKey.findMany.mockResolvedValue([]);\n\n const response = await request(app)\n .get('/api/v1/merchants/api-keys')\n .set('Authorization', `Bearer ${accessToken}`);\n\n expect(response.status).toBe(200);\n expect(prismaMock.merchant.findUnique).toHaveBeenCalledWith({ where: { id: merchant.id } });\n expect(prismaMock.refreshToken.findUnique).not.toHaveBeenCalled();\n });\n\n test('accepts valid API keys on merchant routes and updates lastUsedAt', async () => {\n prismaMock.apiKey.findUnique.mockResolvedValue({\n ...apiKeyRecord,\n merchant,\n } as any);\n prismaMock.apiKey.update.mockResolvedValue(apiKeyRecord as any);\n prismaMock.invoice.findMany.mockResolvedValue([]);\n\n const response = await request(app)\n .get('/api/v1/invoices')\n .set('Authorization', `Bearer ${rawApiKey}`);\n\n expect(response.status).toBe(200);\n expect(prismaMock.apiKey.update).toHaveBeenCalledWith({\n where: { id: 'key-1' },\n data: { lastUsedAt: expect.any(Date) },\n });\n });\n\n test('rejects API keys on key-management routes', async () => {\n prismaMock.apiKey.findUnique.mockResolvedValue({\n ...apiKeyRecord,\n merchant,\n } as any);\n prismaMock.apiKey.update.mockResolvedValue(apiKeyRecord as any);\n prismaMock.apiKey.findMany.mockResolvedValue([apiKeyRecord] as any);\n\n const response = await request(app)\n .get('/api/v1/merchants/api-keys')\n .set('Authorization', `Bearer ${rawApiKey}`);\n\n expect(response.status).toBe(401);\n expect(prismaMock.apiKey.findMany).not.toHaveBeenCalled();\n });\n\n test('returns 401 for unknown API keys', async () => {\n prismaMock.apiKey.findUnique.mockResolvedValue(null);\n\n const response = await request(app)\n .get('/api/v1/merchants/api-keys')\n .set('Authorization', `Bearer ${TEST_UNKNOWN_RAW_API_KEY}`);\n\n expect(response.status).toBe(401);\n });\n\n test('returns 401 for expired API keys', async () => {\n prismaMock.apiKey.findUnique.mockResolvedValue({\n ...apiKeyRecord,\n expiresAt: new Date('2020-01-01T00:00:00.000Z'),\n merchant,\n } as any);\n\n const response = await request(app)\n .get('/api/v1/merchants/api-keys')\n .set('Authorization', `Bearer ${rawApiKey}`);\n\n expect(response.status).toBe(401);\n expect(prismaMock.apiKey.update).not.toHaveBeenCalled();\n });\n\n test('returns 401 when Authorization header is missing', async () => {\n const response = await request(app).get('/api/v1/merchants/api-keys');\n\n expect(response.status).toBe(401);\n });\n\n test('returns 401 when bearer token is empty', async () => {\n const response = await request(app)\n .get('/api/v1/merchants/api-keys')\n .set('Authorization', 'Bearer ');\n\n expect(response.status).toBe(401);\n });\n});\n\ndescribe('API key management security', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n prismaMock.$transaction.mockImplementation(\n async (callback: (tx: typeof prismaMock) => unknown) => callback(prismaMock),\n );\n });\n\n test('POST stores only hash in database, never raw key', async () => {\n prismaMock.refreshToken.findUnique.mockResolvedValue({\n id: 'session-1',\n merchantId: merchant.id,\n token: 'valid-session-token',\n expiresAt: new Date(Date.now() + 60 * 60 * 1000),\n createdAt: new Date(),\n merchant,\n } as any);\n prismaMock.apiKey.count.mockResolvedValue(0);\n prismaMock.apiKey.create.mockImplementation(async (args: any) => ({\n id: 'key-new',\n merchantId: merchant.id,\n keyHash: args.data.keyHash,\n prefix: args.data.prefix,\n name: args.data.name,\n lastUsedAt: null,\n expiresAt: null,\n revokedAt: null,\n createdAt: new Date('2026-06-27T13:00:00.000Z'),\n }));\n\n const response = await request(app)\n .post('/api/v1/merchants/api-keys')\n .set('Authorization', 'Bearer valid-session-token')\n .send({ label: 'Server' });\n\n expect(response.status).toBe(201);\n expect(response.body.key).toMatch(testApiKeyRegex);\n\n const createArgs = prismaMock.apiKey.create.mock.calls[0][0];\n expect(createArgs.data.keyHash).toBe(hashApiKey(response.body.key));\n expect(createArgs.data.keyHash).toHaveLength(64);\n expect(createArgs.data).not.toHaveProperty('key');\n expect(JSON.stringify(createArgs.data)).not.toContain(response.body.key);\n });\n\n test('GET scopes keys to authenticated merchant', async () => {\n prismaMock.refreshToken.findUnique.mockResolvedValue({\n id: 'session-1',\n merchantId: merchant.id,\n token: 'valid-session-token',\n expiresAt: new Date(Date.now() + 60 * 60 * 1000),\n createdAt: new Date(),\n merchant,\n } as any);\n prismaMock.apiKey.findMany.mockResolvedValue([]);\n\n await request(app)\n .get('/api/v1/merchants/api-keys')\n .set('Authorization', 'Bearer valid-session-token');\n\n expect(prismaMock.apiKey.findMany).toHaveBeenCalledWith({\n where: { merchantId: merchant.id, revokedAt: null },\n orderBy: { createdAt: 'desc' },\n select: {\n id: true,\n prefix: true,\n name: true,\n lastUsedAt: true,\n createdAt: true,\n },\n });\n });\n\n test('DELETE returns 400 when key is already revoked', async () => {\n prismaMock.refreshToken.findUnique.mockResolvedValue({\n id: 'session-1',\n merchantId: merchant.id,\n token: 'valid-session-token',\n expiresAt: new Date(Date.now() + 60 * 60 * 1000),\n createdAt: new Date(),\n merchant,\n } as any);\n prismaMock.apiKey.findFirst.mockResolvedValue({\n ...apiKeyRecord,\n revokedAt: new Date('2026-06-27T11:00:00.000Z'),\n } as any);\n\n const response = await request(app)\n .delete('/api/v1/merchants/api-keys/key-1')\n .set('Authorization', 'Bearer valid-session-token');\n\n expect(response.status).toBe(400);\n expect(response.body).toEqual({ error: 'API key already revoked' });\n expect(prismaMock.apiKey.update).not.toHaveBeenCalled();\n });\n\n test('revoked key cannot authenticate subsequent requests', async () => {\n prismaMock.apiKey.findUnique.mockResolvedValue({\n ...apiKeyRecord,\n revokedAt: new Date('2026-06-27T14:00:00.000Z'),\n merchant,\n } as any);\n\n const response = await request(app)\n .get('/api/v1/invoices')\n .set('Authorization', `Bearer ${rawApiKey}`);\n\n expect(response.status).toBe(401);\n });\n\n test('API key cannot create additional API keys', async () => {\n prismaMock.apiKey.findUnique.mockResolvedValue({\n ...apiKeyRecord,\n merchant,\n } as any);\n\n const response = await request(app)\n .post('/api/v1/merchants/api-keys')\n .set('Authorization', `Bearer ${rawApiKey}`)\n .send({ label: 'Secondary' });\n\n expect(response.status).toBe(401);\n expect(prismaMock.apiKey.create).not.toHaveBeenCalled();\n expect(prismaMock.refreshToken.findUnique).not.toHaveBeenCalled();\n });\n});\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\integration\\auth.routes.test.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 10, + "column": 5, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 10, + "endColumn": 20 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 15, + "column": 9, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 15, + "endColumn": 17 + } + ], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 2, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import { jest, beforeEach } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\nimport request from 'supertest';\n\nconst mockVerify = { returns: true };\nconst mockKeypairError = { throws: false };\n\njest.unstable_mockModule('@stellar/stellar-sdk', () => ({\n Keypair: {\n fromPublicKey: () => {\n if (mockKeypairError.throws) {\n throw new Error('invalid public key');\n }\n return {\n verify: () => mockVerify.returns,\n };\n },\n },\n}));\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { default: app } = await import('../../src/app.js');\n\nconst address = 'GABCDEF123';\nconst nonce = 'nonce-123';\nconst signature = 'deadbeef';\nconst mockDate = new Date('2026-06-21T12:00:00Z');\n\ndescribe('Auth Routes', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n jest.useFakeTimers({ now: mockDate });\n mockVerify.returns = true;\n mockKeypairError.throws = false;\n });\n\n afterEach(() => {\n jest.useRealTimers();\n });\n\n describe('POST /api/v1/auth/verify', () => {\n const mockAuthNonce = {\n id: 'uuid-1',\n address,\n nonce,\n message: `Shade Authentication\\nAddress: ${address}\\nNonce: ${nonce}\\nTimestamp: 2026-06-21T12:00:00.000Z`,\n expiresAt: new Date('2026-06-21T12:05:00.000Z'),\n usedAt: null,\n createdAt: mockDate,\n merchantId: null,\n };\n\n test('should return 200 with tokens for a valid signature (new merchant)', async () => {\n prismaMock.authNonce.findUnique.mockResolvedValue(mockAuthNonce);\n prismaMock.merchant.findFirst.mockResolvedValue(null);\n prismaMock.merchant.create.mockResolvedValue({\n id: 'merchant-uuid',\n merchantId: 123456,\n address,\n account: null,\n email: null,\n firstName: null,\n lastName: null,\n businessName: null,\n category: null,\n description: null,\n logo: null,\n webhook: null,\n active: true,\n verified: false,\n emailVerified: false,\n registered: false,\n createdAt: mockDate,\n updatedAt: mockDate,\n });\n prismaMock.refreshToken.create.mockResolvedValue({\n id: 'session-uuid',\n merchantId: 'merchant-uuid',\n token: 'refresh-uuid',\n expiresAt: new Date('2026-06-28T12:00:00.000Z'),\n createdAt: mockDate,\n });\n prismaMock.authNonce.update.mockResolvedValue(mockAuthNonce);\n\n const response = await request(app)\n .post('/api/v1/auth/verify')\n .send({ address, nonce, signature });\n\n expect(response.status).toBe(200);\n expect(response.body).toMatchObject({\n accessToken: expect.any(String),\n refreshToken: expect.any(String),\n merchant: {\n id: 'merchant-uuid',\n address,\n isRegistered: false,\n },\n });\n });\n\n test('should return 200 with isRegistered: true for an existing merchant with firstName', async () => {\n prismaMock.authNonce.findUnique.mockResolvedValue(mockAuthNonce);\n prismaMock.merchant.findFirst.mockResolvedValue({\n id: 'merchant-uuid',\n merchantId: 123456,\n address,\n account: null,\n email: 'test@merchant.com',\n firstName: 'Jane',\n lastName: 'Doe',\n businessName: 'Acme',\n category: 'retail',\n description: null,\n logo: null,\n webhook: null,\n active: true,\n verified: true,\n emailVerified: true,\n registered: true,\n createdAt: mockDate,\n updatedAt: mockDate,\n });\n prismaMock.refreshToken.create.mockResolvedValue({\n id: 'session-uuid',\n merchantId: 'merchant-uuid',\n token: 'refresh-uuid',\n expiresAt: new Date('2026-06-28T12:00:00.000Z'),\n createdAt: mockDate,\n });\n prismaMock.authNonce.update.mockResolvedValue(mockAuthNonce);\n\n const response = await request(app)\n .post('/api/v1/auth/verify')\n .send({ address, nonce, signature });\n\n expect(response.status).toBe(200);\n expect(response.body.merchant).toMatchObject({\n isRegistered: true,\n });\n });\n\n test('should return 401 for an invalid signature', async () => {\n mockVerify.returns = false;\n prismaMock.authNonce.findUnique.mockResolvedValue(mockAuthNonce);\n\n const response = await request(app)\n .post('/api/v1/auth/verify')\n .send({ address, nonce, signature });\n\n expect(response.status).toBe(401);\n expect(response.body).toMatchObject({ error: 'Signature verification failed' });\n });\n\n test('should return 401 for an expired nonce', async () => {\n jest.setSystemTime(new Date('2026-06-21T12:10:00.000Z'));\n prismaMock.authNonce.findUnique.mockResolvedValue(mockAuthNonce);\n\n const response = await request(app)\n .post('/api/v1/auth/verify')\n .send({ address, nonce, signature });\n\n expect(response.status).toBe(401);\n expect(response.body).toMatchObject({ error: 'Nonce expired' });\n });\n\n test('should return 401 for a replayed (already used) nonce', async () => {\n prismaMock.authNonce.findUnique.mockResolvedValue({\n ...mockAuthNonce,\n usedAt: new Date('2026-06-21T12:01:00.000Z'),\n });\n\n const response = await request(app)\n .post('/api/v1/auth/verify')\n .send({ address, nonce, signature });\n\n expect(response.status).toBe(401);\n expect(response.body).toMatchObject({ error: 'Nonce already used' });\n });\n\n test('should return 400 when required fields are missing', async () => {\n const response = await request(app).post('/api/v1/auth/verify').send({});\n\n expect(response.status).toBe(400);\n });\n\n test('should return 400 when fields are not strings', async () => {\n const response = await request(app)\n .post('/api/v1/auth/verify')\n .send({ address: 123, nonce: true, signature: [] });\n\n expect(response.status).toBe(400);\n });\n\n test('should return 401 when nonce is not found', async () => {\n prismaMock.authNonce.findUnique.mockResolvedValue(null);\n\n const response = await request(app)\n .post('/api/v1/auth/verify')\n .send({ address, nonce, signature });\n\n expect(response.status).toBe(401);\n expect(response.body).toMatchObject({ error: 'Nonce not found' });\n });\n\n test('should return 401 when signing address does not match the nonce address', async () => {\n prismaMock.authNonce.findUnique.mockResolvedValue(mockAuthNonce);\n\n const response = await request(app)\n .post('/api/v1/auth/verify')\n .send({ address: 'GWRONG', nonce, signature });\n\n expect(response.status).toBe(401);\n expect(response.body).toMatchObject({ error: 'Address mismatch' });\n });\n });\n});\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\integration\\invoice.routes.test.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 60, + "column": 25, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 60, + "endColumn": 27 + } + ], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 1, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import { jest } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\nimport jwt from 'jsonwebtoken';\nimport request from 'supertest';\n\nconst sendInvoiceEmailMock = jest.fn(async () => undefined);\n\njest.unstable_mockModule('../../src/services/email.service.js', () => ({\n __esModule: true,\n sendOtp: jest.fn(async () => undefined),\n sendInvoiceEmail: sendInvoiceEmailMock,\n}));\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { environment } = await import('../../src/config/environment.js');\nconst { default: app } = await import('../../src/app.js');\n\nconst MERCHANT_ID = 'merchant-1';\n\nconst merchant = {\n id: MERCHANT_ID,\n merchantId: 1,\n address: '0x123',\n account: null,\n email: 'merchant@example.com',\n firstName: null,\n lastName: null,\n businessName: null,\n category: null,\n description: null,\n logo: null,\n webhook: null,\n active: true,\n verified: false,\n emailVerified: false,\n registered: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n};\n\nconst baseInvoice = {\n id: 'invoice-1',\n invoiceId: null,\n paymentSlug: 'aZ09-_slug',\n description: 'Website design',\n amount: 5000n,\n token: 'USDC',\n merchantId: MERCHANT_ID,\n status: 'PENDING',\n ref: null,\n payer: null,\n payerEmail: null,\n email: null,\n expiresAt: null,\n datePaid: null,\n createdAt: new Date('2026-01-01T00:00:00.000Z'),\n updatedAt: new Date('2026-01-01T00:00:00.000Z'),\n};\n\nconst authenticate = () => {\n prismaMock.merchant.findUnique.mockResolvedValue(merchant as any);\n};\n\nconst accessToken = jwt.sign(\n { sub: MERCHANT_ID, address: merchant.address },\n environment.jwtSecret,\n);\nconst auth = { Authorization: `Bearer ${accessToken}` };\n\ndescribe('Invoice routes', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n sendInvoiceEmailMock.mockClear();\n });\n\n describe('POST /api/v1/invoices', () => {\n test('returns 401 when unauthenticated', async () => {\n const response = await request(app)\n .post('/api/v1/invoices')\n .send({ description: 'x', amount: '100', token: 'USDC' });\n\n expect(response.status).toBe(401);\n expect(prismaMock.invoice.create).not.toHaveBeenCalled();\n });\n\n test('returns 201 with a unique url-safe paymentSlug', async () => {\n authenticate();\n prismaMock.invoice.create.mockImplementation(async (args: any) => ({\n ...baseInvoice,\n ...args.data,\n }));\n\n const response = await request(app)\n .post('/api/v1/invoices')\n .set(auth)\n .send({ description: 'Website design', amount: '5000', token: 'USDC' });\n\n expect(response.status).toBe(201);\n expect(response.body.status).toBe('PENDING');\n expect(response.body.amount).toBe('5000');\n expect(response.body.paymentSlug).toMatch(/^[A-Za-z0-9_-]+$/);\n });\n\n test('creates a DRAFT invoice when isDraft is true', async () => {\n authenticate();\n prismaMock.invoice.create.mockImplementation(async (args: any) => ({\n ...baseInvoice,\n ...args.data,\n }));\n\n const response = await request(app)\n .post('/api/v1/invoices')\n .set(auth)\n .send({ description: 'Draft', amount: '5000', token: 'USDC', isDraft: true });\n\n expect(response.status).toBe(201);\n expect(response.body.status).toBe('DRAFT');\n });\n\n test('returns 400 when amount is not positive or token is empty', async () => {\n authenticate();\n\n const response = await request(app)\n .post('/api/v1/invoices')\n .set(auth)\n .send({ description: 'x', amount: -5, token: '' });\n\n expect(response.status).toBe(400);\n expect(response.body.errors).toMatchObject({\n amount: expect.any(String),\n token: expect.any(String),\n });\n expect(prismaMock.invoice.create).not.toHaveBeenCalled();\n });\n });\n\n describe('GET /api/v1/invoices', () => {\n test('returns a paginated list scoped to the merchant with status filter', async () => {\n authenticate();\n prismaMock.invoice.findMany.mockResolvedValue([baseInvoice] as any);\n prismaMock.invoice.count.mockResolvedValue(1 as any);\n\n const response = await request(app)\n .get('/api/v1/invoices?status=PENDING&limit=10&offset=0')\n .set(auth);\n\n expect(response.status).toBe(200);\n expect(response.body.data).toHaveLength(1);\n expect(response.body.pagination).toEqual({ limit: 10, offset: 0, total: 1 });\n\n const findArgs = prismaMock.invoice.findMany.mock.calls[0][0];\n expect(findArgs.where).toMatchObject({ merchantId: MERCHANT_ID, status: 'PENDING' });\n });\n\n test('clamps limit to the maximum of 100', async () => {\n authenticate();\n prismaMock.invoice.findMany.mockResolvedValue([] as any);\n prismaMock.invoice.count.mockResolvedValue(0 as any);\n\n const response = await request(app).get('/api/v1/invoices?limit=500').set(auth);\n\n expect(response.status).toBe(200);\n expect(response.body.pagination.limit).toBe(100);\n });\n });\n\n describe('GET /api/v1/invoices/:id', () => {\n test('returns 200 when the invoice belongs to the merchant', async () => {\n authenticate();\n prismaMock.invoice.findFirst.mockResolvedValue(baseInvoice as any);\n\n const response = await request(app).get('/api/v1/invoices/invoice-1').set(auth);\n\n expect(response.status).toBe(200);\n expect(response.body.id).toBe('invoice-1');\n });\n\n test('returns 404 when the invoice is missing or owned by another merchant', async () => {\n authenticate();\n prismaMock.invoice.findFirst.mockResolvedValue(null);\n\n const response = await request(app).get('/api/v1/invoices/other').set(auth);\n\n expect(response.status).toBe(404);\n });\n });\n\n describe('PATCH /api/v1/invoices/:id/void', () => {\n test('voids a PENDING invoice', async () => {\n authenticate();\n prismaMock.invoice.findFirst.mockResolvedValue(baseInvoice as any);\n prismaMock.invoice.update.mockResolvedValue({\n ...baseInvoice,\n status: 'CANCELLED',\n } as any);\n\n const response = await request(app).patch('/api/v1/invoices/invoice-1/void').set(auth);\n\n expect(response.status).toBe(200);\n expect(response.body.status).toBe('CANCELLED');\n });\n\n test('returns 400 when voiding a non-PENDING invoice', async () => {\n authenticate();\n prismaMock.invoice.findFirst.mockResolvedValue({\n ...baseInvoice,\n status: 'PAID',\n } as any);\n\n const response = await request(app).patch('/api/v1/invoices/invoice-1/void').set(auth);\n\n expect(response.status).toBe(400);\n expect(prismaMock.invoice.update).not.toHaveBeenCalled();\n });\n });\n\n describe('GET /api/v1/invoices/:id/pdf', () => {\n test('returns 401 when unauthenticated', async () => {\n const response = await request(app).get('/api/v1/invoices/invoice-1/pdf');\n\n expect(response.status).toBe(401);\n });\n\n test('returns 404 when the invoice is missing or owned by another merchant', async () => {\n authenticate();\n prismaMock.invoice.findFirst.mockResolvedValue(null);\n\n const response = await request(app).get('/api/v1/invoices/other/pdf').set(auth);\n\n expect(response.status).toBe(404);\n });\n\n test('streams a real PDF scoped to the authenticated merchant, never touching disk', async () => {\n authenticate();\n prismaMock.invoice.findFirst.mockResolvedValue({ ...baseInvoice, merchant } as any);\n\n const response = await request(app).get('/api/v1/invoices/invoice-1/pdf').set(auth);\n\n expect(response.status).toBe(200);\n expect(response.headers['content-type']).toContain('application/pdf');\n expect(response.headers['content-disposition']).toBe(\n `attachment; filename=\"invoice-${baseInvoice.paymentSlug}.pdf\"`,\n );\n const body = response.body as Buffer;\n expect(Buffer.isBuffer(body)).toBe(true);\n expect(body.subarray(0, 5).toString('ascii')).toBe('%PDF-');\n\n const findArgs = prismaMock.invoice.findFirst.mock.calls[0][0];\n expect(findArgs.where).toMatchObject({ id: 'invoice-1', merchantId: MERCHANT_ID });\n });\n });\n\n describe('POST /api/v1/invoices/:id/send', () => {\n test('returns 401 when unauthenticated', async () => {\n const response = await request(app).post('/api/v1/invoices/invoice-1/send');\n\n expect(response.status).toBe(401);\n expect(sendInvoiceEmailMock).not.toHaveBeenCalled();\n });\n\n test('returns 404 when the invoice is missing or owned by another merchant', async () => {\n authenticate();\n prismaMock.invoice.findFirst.mockResolvedValue(null);\n\n const response = await request(app).post('/api/v1/invoices/other/send').set(auth);\n\n expect(response.status).toBe(404);\n expect(sendInvoiceEmailMock).not.toHaveBeenCalled();\n });\n\n test('returns 400 and does not attempt to send when the invoice has no email set', async () => {\n authenticate();\n prismaMock.invoice.findFirst.mockResolvedValue({\n ...baseInvoice,\n email: null,\n merchant,\n } as any);\n\n const response = await request(app).post('/api/v1/invoices/invoice-1/send').set(auth);\n\n expect(response.status).toBe(400);\n expect(sendInvoiceEmailMock).not.toHaveBeenCalled();\n });\n\n test('sends the invoice email when invoice.email is set', async () => {\n authenticate();\n const invoiceWithEmail = { ...baseInvoice, email: 'payer@example.com', merchant };\n prismaMock.invoice.findFirst.mockResolvedValue(invoiceWithEmail as any);\n\n const response = await request(app).post('/api/v1/invoices/invoice-1/send').set(auth);\n\n expect(response.status).toBe(200);\n expect(sendInvoiceEmailMock).toHaveBeenCalledWith(invoiceWithEmail, merchant);\n });\n });\n});\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\integration\\merchant.profile.test.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/no-unused-vars", + "severity": 2, + "message": "'jest' is defined but never used. Allowed unused vars must match /^_/u.", + "line": 1, + "column": 10, + "nodeType": null, + "messageId": "unusedVar", + "endLine": 1, + "endColumn": 14 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 34, + "column": 60, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 34, + "endColumn": 62 + } + ], + "suppressedMessages": [], + "errorCount": 1, + "fatalErrorCount": 0, + "warningCount": 1, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import { jest } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\nimport request from 'supertest';\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { default: app } = await import('../../src/app.js');\n\nconst ME_URL = '/api/v1/merchants/me';\n\nconst baseMerchant = {\n id: 'uuid-1',\n merchantId: 1,\n address: '0x123',\n account: 'CCONTRACT',\n email: 'ada@example.com',\n firstName: 'Ada',\n lastName: 'Lovelace',\n businessName: 'Analytical Engines',\n category: 'software',\n description: 'We build computing machines.',\n logo: null,\n webhook: null,\n active: true,\n verified: false,\n emailVerified: false,\n registered: true,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n // Internal relations that the sanitizer allow-list must strip from responses.\n refreshTokens: [{ id: 'rt-1', token: 'secret-token' }],\n apiKeys: [{ id: 'ak-1', keyHash: 'hashed-secret' }],\n};\n\nconst authenticateAs = (merchant: Record) => {\n prismaMock.refreshToken.findUnique.mockResolvedValue({\n id: 'session-1',\n merchantId: merchant.id,\n token: 'valid-token',\n expiresAt: new Date(Date.now() + 60 * 60 * 1000),\n createdAt: new Date(),\n merchant,\n } as any);\n};\n\ndescribe('GET /api/v1/merchants/me', () => {\n beforeEach(() => mockReset(prismaMock));\n\n test('returns 401 when unauthenticated', async () => {\n const response = await request(app).get(ME_URL);\n expect(response.status).toBe(401);\n });\n\n test('returns 200 with the full profile and no internal fields', async () => {\n authenticateAs(baseMerchant);\n prismaMock.merchant.findUnique.mockResolvedValue(baseMerchant as any);\n\n const response = await request(app).get(ME_URL).set('Authorization', 'Bearer valid-token');\n\n expect(response.status).toBe(200);\n expect(response.body).toMatchObject({ id: 'uuid-1', account: 'CCONTRACT', webhook: null });\n expect(response.body).not.toHaveProperty('refreshTokens');\n expect(response.body).not.toHaveProperty('apiKeys');\n });\n});\n\ndescribe('PATCH /api/v1/merchants/me', () => {\n beforeEach(() => mockReset(prismaMock));\n\n test('returns 401 when unauthenticated', async () => {\n const response = await request(app).patch(ME_URL).send({ firstName: 'Grace' });\n expect(response.status).toBe(401);\n expect(prismaMock.merchant.update).not.toHaveBeenCalled();\n });\n\n test('updates a valid partial payload and returns 200', async () => {\n authenticateAs(baseMerchant);\n prismaMock.merchant.update.mockImplementation(async (args: any) => ({\n ...baseMerchant,\n ...args.data,\n }));\n\n const response = await request(app)\n .patch(ME_URL)\n .set('Authorization', 'Bearer valid-token')\n .send({ firstName: 'Grace', webhook: 'https://example.com/hook' });\n\n expect(response.status).toBe(200);\n expect(response.body).toMatchObject({\n firstName: 'Grace',\n webhook: 'https://example.com/hook',\n });\n });\n\n test('silently ignores non-editable fields (address/email/merchantId/account)', async () => {\n authenticateAs(baseMerchant);\n prismaMock.merchant.update.mockImplementation(async (args: any) => ({\n ...baseMerchant,\n ...args.data,\n }));\n\n const response = await request(app)\n .patch(ME_URL)\n .set('Authorization', 'Bearer valid-token')\n .send({\n firstName: 'Grace',\n address: '0xHACK',\n email: 'evil@example.com',\n merchantId: 999,\n account: '0xHACKED',\n });\n\n expect(response.status).toBe(200);\n const updateArg = prismaMock.merchant.update.mock.calls[0][0];\n expect(updateArg.data).toEqual({ firstName: 'Grace' });\n expect(response.body.address).toBe('0x123');\n expect(response.body.email).toBe('ada@example.com');\n expect(response.body.merchantId).toBe(1);\n expect(response.body.account).toBe('CCONTRACT');\n });\n\n test('returns 400 for an invalid (non-HTTPS) webhook', async () => {\n authenticateAs(baseMerchant);\n\n const response = await request(app)\n .patch(ME_URL)\n .set('Authorization', 'Bearer valid-token')\n .send({ webhook: 'http://example.com/hook' });\n\n expect(response.status).toBe(400);\n expect(response.body.error).toBe('Validation failed');\n expect(prismaMock.merchant.update).not.toHaveBeenCalled();\n });\n\n test('clears the webhook when sent null', async () => {\n authenticateAs(baseMerchant);\n prismaMock.merchant.update.mockImplementation(async (args: any) => ({\n ...baseMerchant,\n ...args.data,\n }));\n\n const response = await request(app)\n .patch(ME_URL)\n .set('Authorization', 'Bearer valid-token')\n .send({ webhook: null });\n\n expect(response.status).toBe(200);\n expect(response.body.webhook).toBeNull();\n });\n\n test('returns 400 for a required text field sent empty', async () => {\n authenticateAs(baseMerchant);\n\n const response = await request(app)\n .patch(ME_URL)\n .set('Authorization', 'Bearer valid-token')\n .send({ firstName: '' });\n\n expect(response.status).toBe(400);\n expect(prismaMock.merchant.update).not.toHaveBeenCalled();\n });\n\n test('returns 400 for an empty payload', async () => {\n authenticateAs(baseMerchant);\n\n const response = await request(app)\n .patch(ME_URL)\n .set('Authorization', 'Bearer valid-token')\n .send({});\n\n expect(response.status).toBe(400);\n expect(prismaMock.merchant.update).not.toHaveBeenCalled();\n });\n});\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\integration\\merchant.register.test.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 16, + "column": 3, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 16, + "endColumn": 16 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 17, + "column": 3, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 17, + "endColumn": 18 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 18, + "column": 3, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 18, + "endColumn": 24 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 28, + "column": 54, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 28, + "endColumn": 56 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 65, + "column": 60, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 65, + "endColumn": 62 + } + ], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 5, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import { jest } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\nimport jwt from 'jsonwebtoken';\nimport request from 'supertest';\n\nconst sendOtpMock = jest.fn(async () => undefined);\n\njest.unstable_mockModule('../../src/services/email.service.js', () => ({\n __esModule: true,\n sendOtp: sendOtpMock,\n sendInvoiceEmail: jest.fn(async () => undefined),\n}));\n\njest.unstable_mockModule('../../src/services/otp.services.js', () => ({\n __esModule: true,\n generateOtp: () => '123456',\n hashOtp: async () => 'hashed-otp',\n verifyOtpHash: async () => true,\n issueEmailOtp: jest.fn(),\n verifyEmailOtp: jest.fn(),\n resendEmailOtp: jest.fn(),\n}));\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { environment } = await import('../../src/config/environment.js');\nconst { default: app } = await import('../../src/app.js');\n\nconst tokenFor = (merchant: Record) =>\n jwt.sign({ sub: merchant.id as string }, environment.jwtSecret);\n\nconst REGISTER_URL = '/api/v1/merchants/register';\n\nconst baseMerchant = {\n id: 'uuid-1',\n merchantId: 1,\n address: '0x123',\n account: null,\n email: null,\n firstName: null,\n lastName: null,\n businessName: null,\n category: null,\n description: null,\n logo: null,\n webhook: null,\n active: true,\n verified: false,\n emailVerified: false,\n registered: false,\n emailOtp: null,\n emailOtpExpiresAt: null,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n};\n\nconst validPayload = {\n firstName: 'Ada',\n lastName: 'Lovelace',\n email: 'ada@example.com',\n businessName: 'Analytical Engines',\n category: 'software',\n description: 'We build computing machines.',\n};\n\nconst authenticateAs = (merchant: Record) => {\n prismaMock.merchant.findUnique.mockResolvedValue(merchant as any);\n};\n\nconst authHeader = `Bearer ${tokenFor(baseMerchant)}`;\n\ndescribe('POST /api/v1/merchants/register', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n sendOtpMock.mockClear();\n });\n\n test('returns 401 for unauthenticated requests', async () => {\n const response = await request(app).post(REGISTER_URL).send(validPayload);\n\n expect(response.status).toBe(401);\n expect(response.body).toEqual({ error: 'Authentication required' });\n expect(prismaMock.merchant.update).not.toHaveBeenCalled();\n });\n\n test('returns 401 when the token is invalid', async () => {\n prismaMock.refreshToken.findUnique.mockResolvedValue(null);\n\n const response = await request(app)\n .post(REGISTER_URL)\n .set('Authorization', 'Bearer bad-token')\n .send(validPayload);\n\n expect(response.status).toBe(401);\n expect(response.body).toEqual({ error: 'Invalid or expired token' });\n });\n\n test('returns 200 with the merchant profile on valid payload', async () => {\n authenticateAs(baseMerchant);\n prismaMock.merchant.findUnique.mockResolvedValue(baseMerchant as any);\n prismaMock.merchant.findFirst.mockResolvedValue(null);\n prismaMock.merchant.update.mockImplementation(async (args: any) => ({\n ...baseMerchant,\n ...args.data,\n }));\n\n const response = await request(app)\n .post(REGISTER_URL)\n .set('Authorization', authHeader)\n .send(validPayload);\n\n expect(response.status).toBe(200);\n expect(response.body).toMatchObject({\n id: 'uuid-1',\n firstName: 'Ada',\n lastName: 'Lovelace',\n email: 'ada@example.com',\n businessName: 'Analytical Engines',\n emailVerified: false,\n registered: true,\n });\n expect(sendOtpMock).toHaveBeenCalledWith('ada@example.com', '123456', 'Ada');\n });\n\n test('returns 409 when the email is already registered', async () => {\n authenticateAs(baseMerchant);\n prismaMock.merchant.findUnique.mockResolvedValue(baseMerchant as any);\n prismaMock.merchant.findFirst.mockResolvedValue({\n ...baseMerchant,\n id: 'uuid-2',\n } as any);\n\n const response = await request(app)\n .post(REGISTER_URL)\n .set('Authorization', authHeader)\n .send(validPayload);\n\n expect(response.status).toBe(409);\n expect(response.body).toEqual({ error: 'Email already registered' });\n });\n\n test('returns 409 when the merchant already completed registration', async () => {\n const registeredMerchant = { ...baseMerchant, registered: true };\n authenticateAs(registeredMerchant);\n prismaMock.merchant.findUnique.mockResolvedValue(registeredMerchant as any);\n\n const response = await request(app)\n .post(REGISTER_URL)\n .set('Authorization', authHeader)\n .send(validPayload);\n\n expect(response.status).toBe(409);\n expect(response.body).toEqual({ error: 'Profile already set up' });\n });\n\n test('returns 400 with field-level errors when required fields are missing', async () => {\n authenticateAs(baseMerchant);\n\n const response = await request(app)\n .post(REGISTER_URL)\n .set('Authorization', authHeader)\n .send({ email: 'not-an-email' });\n\n expect(response.status).toBe(400);\n expect(response.body.error).toBe('Validation failed');\n expect(response.body.errors).toMatchObject({\n firstName: expect.any(String),\n lastName: expect.any(String),\n email: expect.any(String),\n businessName: expect.any(String),\n category: expect.any(String),\n description: expect.any(String),\n });\n expect(prismaMock.merchant.update).not.toHaveBeenCalled();\n });\n});\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\integration\\merchant.routes.test.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/no-unused-vars", + "severity": 2, + "message": "'jest' is defined but never used. Allowed unused vars must match /^_/u.", + "line": 1, + "column": 10, + "nodeType": null, + "messageId": "unusedVar", + "endLine": 1, + "endColumn": 14 + } + ], + "suppressedMessages": [], + "errorCount": 1, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import { jest } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\nimport request from 'supertest';\n\n// Wait for the mock to be applied\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { default: app } = await import('../../src/app.js');\n\ndescribe('Merchant Routes', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n });\n\n test('POST /api/v1/merchants should create a merchant', async () => {\n const merchantData = {\n merchantId: 1,\n address: '0x123',\n email: 'test@example.com',\n };\n\n const expectedMerchant = {\n id: 'uuid-1',\n ...merchantData,\n active: true,\n verified: false,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n };\n\n prismaMock.merchant.create.mockResolvedValue(expectedMerchant as any);\n\n const response = await request(app).post('/api/v1/merchants').send(merchantData);\n\n expect(response.status).toBe(201);\n expect(response.body).toEqual(expectedMerchant);\n });\n\n test('GET /api/v1/merchants/:id should return a merchant', async () => {\n const expectedMerchant = {\n id: 'uuid-1',\n merchantId: 1,\n address: '0x123',\n email: 'test@example.com',\n active: true,\n verified: false,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n };\n\n prismaMock.merchant.findUnique.mockResolvedValue(expectedMerchant as any);\n\n const response = await request(app).get('/api/v1/merchants/1');\n\n expect(response.status).toBe(200);\n expect(response.body).toEqual(expectedMerchant);\n });\n\n test('GET /api/v1/merchants should list merchants', async () => {\n const merchants = [\n {\n id: 'uuid-1',\n merchantId: 1,\n address: '0x1',\n email: '1',\n active: true,\n verified: false,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n },\n ];\n\n prismaMock.merchant.findMany.mockResolvedValue(merchants as any);\n\n const response = await request(app).get('/api/v1/merchants?limit=10&offset=0');\n\n expect(response.status).toBe(200);\n expect(response.body).toEqual(merchants);\n });\n});\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\integration\\merchant.signing-key.test.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 14, + "column": 5, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 14, + "endColumn": 20 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 15, + "column": 5, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 15, + "endColumn": 17 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 16, + "column": 5, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 16, + "endColumn": 29 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 49, + "column": 36, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 49, + "endColumn": 38 + } + ], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 4, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import { jest } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\nimport request from 'supertest';\nimport { TEST_RAW_API_KEY } from '../helpers/api-key.fixtures.js';\n\njest.unstable_mockModule('../../src/utils/api-key.utils.js', () => {\n const prefix = 'sk_' + 'live_';\n return {\n __esModule: true,\n API_KEY_PREFIX: prefix,\n API_KEY_RANDOM_LENGTH: 32,\n API_KEY_DISPLAY_PREFIX_LENGTH: 8,\n MAX_ACTIVE_API_KEYS: 10,\n isApiKeyToken: (token: string) => token.startsWith(prefix),\n hashApiKey: (value: string) => `hash-${value}`,\n generateApiKeyMaterial: () => ({ rawKey: 'x', prefix: 'x', keyHash: 'x' }),\n };\n});\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { default: app } = await import('../../src/app.js');\n\nconst HEX32 = /^[0-9a-f]{64}$/;\n\nconst merchant = {\n id: 'merchant-1',\n merchantId: 1,\n address: '0x123',\n account: null,\n merchantKey: null,\n email: 'merchant@example.com',\n firstName: 'Ada',\n lastName: 'Lovelace',\n businessName: 'Engines',\n category: 'software',\n description: 'desc',\n logo: null,\n webhook: null,\n active: true,\n verified: false,\n emailVerified: true,\n registered: true,\n emailOtp: null,\n emailOtpExpiresAt: null,\n createdAt: new Date('2026-06-27T12:00:00.000Z'),\n updatedAt: new Date('2026-06-27T12:00:00.000Z'),\n};\n\nconst authenticateWithSession = () => {\n prismaMock.refreshToken.findUnique.mockResolvedValue({\n id: 'session-1',\n merchantId: merchant.id,\n token: 'valid-token',\n expiresAt: new Date(Date.now() + 60 * 60 * 1000),\n createdAt: new Date(),\n merchant,\n } as any);\n};\n\ndescribe('POST /api/v1/merchants/signing-key', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n jest.spyOn(console, 'info').mockImplementation(() => {});\n });\n\n afterEach(() => {\n jest.restoreAllMocks();\n });\n\n test('returns 401 when unauthenticated', async () => {\n const response = await request(app).post('/api/v1/merchants/signing-key');\n\n expect(response.status).toBe(401);\n expect(prismaMock.merchant.update).not.toHaveBeenCalled();\n });\n\n test('returns 401 when authenticated with an API key (session-only)', async () => {\n const response = await request(app)\n .post('/api/v1/merchants/signing-key')\n .set('Authorization', `Bearer ${TEST_RAW_API_KEY}`);\n\n expect(response.status).toBe(401);\n expect(prismaMock.merchant.update).not.toHaveBeenCalled();\n });\n\n test('returns 201 with hex public + private, persisting only the public key', async () => {\n authenticateWithSession();\n prismaMock.merchant.findUnique.mockResolvedValue({ ...merchant });\n prismaMock.merchant.updateMany.mockResolvedValue({ count: 1 });\n\n const response = await request(app)\n .post('/api/v1/merchants/signing-key')\n .set('Authorization', 'Bearer valid-token');\n\n expect(response.status).toBe(201);\n expect(response.body.publicKey).toMatch(HEX32);\n expect(response.body.privateKey).toMatch(HEX32);\n\n const updateArgs = prismaMock.merchant.updateMany.mock.calls[0][0];\n expect(updateArgs.data).toEqual({ merchantKey: response.body.publicKey });\n expect(JSON.stringify(updateArgs)).not.toContain(response.body.privateKey);\n });\n});\n\ndescribe('GET /api/v1/merchants/me merchantKey exposure', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n });\n\n test('returns the public merchantKey and never a private key', async () => {\n authenticateWithSession();\n const publicKey = 'a'.repeat(64);\n prismaMock.merchant.findUnique.mockResolvedValue({ ...merchant, merchantKey: publicKey });\n\n const response = await request(app)\n .get('/api/v1/merchants/me')\n .set('Authorization', 'Bearer valid-token');\n\n expect(response.status).toBe(200);\n expect(response.body.merchantKey).toBe(publicKey);\n expect(response.body).not.toHaveProperty('privateKey');\n });\n});\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\integration\\pay.routes.test.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\jest.setup.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\analytics.schema.test.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\api-key.services.test.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 18, + "column": 5, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 18, + "endColumn": 20 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 19, + "column": 5, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 19, + "endColumn": 17 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 20, + "column": 5, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 20, + "endColumn": 29 + } + ], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 3, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import { jest } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\nimport {\n TEST_KEY_HASH,\n TEST_KEY_PREFIX_DISPLAY,\n TEST_RAW_API_KEY,\n} from '../helpers/api-key.fixtures.js';\n\njest.unstable_mockModule('../../src/utils/api-key.utils.js', () => {\n const prefix = 'sk_' + 'live_';\n const rawKey = `${prefix}testkey1234567890123456789012345`;\n return {\n __esModule: true,\n API_KEY_PREFIX: prefix,\n API_KEY_RANDOM_LENGTH: 32,\n API_KEY_DISPLAY_PREFIX_LENGTH: 8,\n MAX_ACTIVE_API_KEYS: 10,\n isApiKeyToken: (token: string) => token.startsWith(prefix),\n hashApiKey: (rawKeyValue: string) => `hash-${rawKeyValue}`,\n generateApiKeyMaterial: () => ({\n rawKey,\n prefix: `${prefix}testkey1`,\n keyHash: `hash-${rawKey}`,\n }),\n };\n});\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { createApiKey, listApiKeys, revokeApiKey, authenticateApiKey } = await import(\n '../../src/services/api-key.services.js'\n);\n\nconst merchantId = 'merchant-1';\nconst baseApiKeyRecord = {\n id: 'key-1',\n merchantId,\n keyHash: TEST_KEY_HASH,\n prefix: TEST_KEY_PREFIX_DISPLAY,\n name: 'Production',\n lastUsedAt: null,\n expiresAt: null,\n revokedAt: null,\n createdAt: new Date('2026-06-27T12:00:00.000Z'),\n};\n\ndescribe('api-key.services', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n prismaMock.$transaction.mockImplementation(\n async (callback: (tx: typeof prismaMock) => unknown) => callback(prismaMock),\n );\n });\n\n test('createApiKey stores hash and returns raw key once', async () => {\n prismaMock.apiKey.count.mockResolvedValue(0);\n prismaMock.apiKey.create.mockResolvedValue(baseApiKeyRecord as any);\n\n const result = await createApiKey(merchantId, 'Production');\n\n expect(prismaMock.apiKey.create).toHaveBeenCalledWith({\n data: {\n merchantId,\n keyHash: TEST_KEY_HASH,\n prefix: TEST_KEY_PREFIX_DISPLAY,\n name: 'Production',\n },\n });\n expect(result).toMatchObject({\n id: 'key-1',\n key: TEST_RAW_API_KEY,\n prefix: TEST_KEY_PREFIX_DISPLAY,\n label: 'Production',\n });\n });\n\n test('createApiKey rejects when active key limit is reached', async () => {\n prismaMock.apiKey.count.mockResolvedValue(10);\n\n await expect(createApiKey(merchantId)).rejects.toMatchObject({\n statusCode: 400,\n message: 'Maximum of 10 active API keys allowed',\n });\n expect(prismaMock.apiKey.create).not.toHaveBeenCalled();\n });\n\n test('listApiKeys returns non-revoked keys without hashes', async () => {\n prismaMock.apiKey.findMany.mockResolvedValue([baseApiKeyRecord] as any);\n\n const result = await listApiKeys(merchantId);\n\n expect(prismaMock.apiKey.findMany).toHaveBeenCalledWith({\n where: { merchantId, revokedAt: null },\n orderBy: { createdAt: 'desc' },\n select: {\n id: true,\n prefix: true,\n name: true,\n lastUsedAt: true,\n createdAt: true,\n },\n });\n expect(result).toEqual([\n {\n id: 'key-1',\n prefix: TEST_KEY_PREFIX_DISPLAY,\n label: 'Production',\n lastUsedAt: null,\n createdAt: baseApiKeyRecord.createdAt,\n },\n ]);\n expect(result[0]).not.toHaveProperty('keyHash');\n expect(result[0]).not.toHaveProperty('key');\n });\n\n test('revokeApiKey marks key as revoked for owning merchant', async () => {\n prismaMock.apiKey.findFirst.mockResolvedValue(baseApiKeyRecord as any);\n prismaMock.apiKey.update.mockResolvedValue({\n ...baseApiKeyRecord,\n revokedAt: new Date(),\n } as any);\n\n await revokeApiKey(merchantId, 'key-1');\n\n expect(prismaMock.apiKey.findFirst).toHaveBeenCalledWith({\n where: { id: 'key-1', merchantId },\n });\n expect(prismaMock.apiKey.update).toHaveBeenCalledWith({\n where: { id: 'key-1' },\n data: { revokedAt: expect.any(Date) },\n });\n });\n\n test('revokeApiKey returns 404 for another merchant key', async () => {\n prismaMock.apiKey.findFirst.mockResolvedValue(null);\n\n await expect(revokeApiKey('merchant-2', 'key-1')).rejects.toMatchObject({\n statusCode: 404,\n });\n });\n\n test('authenticateApiKey updates lastUsedAt and returns merchant', async () => {\n const merchant = { id: merchantId, merchantId: 1, address: '0x123' };\n prismaMock.apiKey.findUnique.mockResolvedValue({\n ...baseApiKeyRecord,\n merchant,\n } as any);\n prismaMock.apiKey.update.mockResolvedValue(baseApiKeyRecord as any);\n\n const result = await authenticateApiKey(TEST_RAW_API_KEY);\n\n expect(prismaMock.apiKey.findUnique).toHaveBeenCalledWith({\n where: { keyHash: TEST_KEY_HASH },\n include: { merchant: true },\n });\n expect(result).toEqual(merchant);\n expect(prismaMock.apiKey.update).toHaveBeenCalledWith({\n where: { id: 'key-1' },\n data: { lastUsedAt: expect.any(Date) },\n });\n });\n\n test('authenticateApiKey returns null for revoked keys', async () => {\n prismaMock.apiKey.findUnique.mockResolvedValue({\n ...baseApiKeyRecord,\n revokedAt: new Date(),\n merchant: { id: merchantId },\n } as any);\n\n const result = await authenticateApiKey(TEST_RAW_API_KEY);\n\n expect(result).toBeNull();\n expect(prismaMock.apiKey.update).not.toHaveBeenCalled();\n });\n\n test('authenticateApiKey returns null for expired keys', async () => {\n prismaMock.apiKey.findUnique.mockResolvedValue({\n ...baseApiKeyRecord,\n expiresAt: new Date('2020-01-01T00:00:00.000Z'),\n merchant: { id: merchantId },\n } as any);\n\n const result = await authenticateApiKey(TEST_RAW_API_KEY);\n\n expect(result).toBeNull();\n expect(prismaMock.apiKey.update).not.toHaveBeenCalled();\n });\n\n test('revokeApiKey rejects already revoked keys', async () => {\n prismaMock.apiKey.findFirst.mockResolvedValue({\n ...baseApiKeyRecord,\n revokedAt: new Date(),\n } as any);\n\n await expect(revokeApiKey(merchantId, 'key-1')).rejects.toMatchObject({\n statusCode: 400,\n message: 'API key already revoked',\n });\n expect(prismaMock.apiKey.update).not.toHaveBeenCalled();\n });\n\n test('countActiveApiKeys excludes expired but non-revoked keys from limit', async () => {\n prismaMock.apiKey.count.mockResolvedValue(9);\n prismaMock.apiKey.create.mockResolvedValue(baseApiKeyRecord as any);\n\n await createApiKey(merchantId, 'Tenth key');\n\n expect(prismaMock.apiKey.count).toHaveBeenCalledWith({\n where: {\n merchantId,\n revokedAt: null,\n OR: [{ expiresAt: null }, { expiresAt: { gt: expect.any(Date) } }],\n },\n });\n expect(prismaMock.apiKey.create).toHaveBeenCalled();\n });\n});\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\api-key.utils.test.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\auth.middleware.test.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 21, + "column": 21, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 21, + "endColumn": 23 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 28, + "column": 23, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 28, + "endColumn": 25 + } + ], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 2, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import { jest } from '@jest/globals';\nimport jwt from 'jsonwebtoken';\nimport type { Request, Response, NextFunction } from 'express';\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { environment } = await import('../../src/config/environment.js');\nconst { authenticateMerchant } = await import('../../src/middlewares/auth.middleware.js');\n\nconst MERCHANT_ID = 'merchant-1';\n\nconst merchant = {\n id: MERCHANT_ID,\n merchantId: 1,\n address: '0x123',\n registered: true,\n};\n\nconst buildReq = (authorization?: string): Request =>\n ({ headers: authorization ? { authorization } : {} }) as unknown as Request;\n\nconst buildRes = () => {\n const res = {} as Response;\n res.status = jest.fn().mockReturnValue(res) as unknown as Response['status'];\n res.json = jest.fn().mockReturnValue(res) as unknown as Response['json'];\n return res;\n};\n\nconst validToken = () =>\n jwt.sign({ sub: MERCHANT_ID, address: merchant.address }, environment.jwtSecret);\n\ndescribe('authenticateMerchant', () => {\n beforeEach(() => {\n jest.clearAllMocks();\n });\n\n test('attaches the merchant and calls next() for a valid JWT', async () => {\n prismaMock.merchant.findUnique.mockResolvedValue(merchant as any);\n const req = buildReq(`Bearer ${validToken()}`);\n const res = buildRes();\n const next = jest.fn() as unknown as NextFunction;\n\n await authenticateMerchant(req, res, next);\n\n expect(prismaMock.merchant.findUnique).toHaveBeenCalledWith({ where: { id: MERCHANT_ID } });\n expect(req.merchant).toEqual(merchant);\n expect(next).toHaveBeenCalledTimes(1);\n expect(res.status).not.toHaveBeenCalled();\n });\n\n test('returns 401 \"Authentication required\" when the Authorization header is missing', async () => {\n const req = buildReq();\n const res = buildRes();\n const next = jest.fn() as unknown as NextFunction;\n\n await authenticateMerchant(req, res, next);\n\n expect(res.status).toHaveBeenCalledWith(401);\n expect(res.json).toHaveBeenCalledWith({ error: 'Authentication required' });\n expect(next).not.toHaveBeenCalled();\n });\n\n test('returns 401 \"Authentication required\" when the scheme is not Bearer', async () => {\n const req = buildReq('Basic abc123');\n const res = buildRes();\n const next = jest.fn() as unknown as NextFunction;\n\n await authenticateMerchant(req, res, next);\n\n expect(res.status).toHaveBeenCalledWith(401);\n expect(res.json).toHaveBeenCalledWith({ error: 'Authentication required' });\n });\n\n test('returns 401 \"Invalid or expired token\" for a malformed token', async () => {\n const req = buildReq('Bearer not-a-real-jwt');\n const res = buildRes();\n const next = jest.fn() as unknown as NextFunction;\n\n await authenticateMerchant(req, res, next);\n\n expect(res.status).toHaveBeenCalledWith(401);\n expect(res.json).toHaveBeenCalledWith({ error: 'Invalid or expired token' });\n expect(prismaMock.merchant.findUnique).not.toHaveBeenCalled();\n });\n\n test('returns 401 \"Invalid or expired token\" for an expired token', async () => {\n const expired = jwt.sign({ sub: MERCHANT_ID }, environment.jwtSecret, { expiresIn: '-1s' });\n const req = buildReq(`Bearer ${expired}`);\n const res = buildRes();\n const next = jest.fn() as unknown as NextFunction;\n\n await authenticateMerchant(req, res, next);\n\n expect(res.status).toHaveBeenCalledWith(401);\n expect(res.json).toHaveBeenCalledWith({ error: 'Invalid or expired token' });\n });\n\n test('returns 401 \"Invalid or expired token\" when the token is signed with the wrong secret', async () => {\n const forged = jwt.sign({ sub: MERCHANT_ID }, 'a-different-secret');\n const req = buildReq(`Bearer ${forged}`);\n const res = buildRes();\n const next = jest.fn() as unknown as NextFunction;\n\n await authenticateMerchant(req, res, next);\n\n expect(res.status).toHaveBeenCalledWith(401);\n expect(res.json).toHaveBeenCalledWith({ error: 'Invalid or expired token' });\n });\n\n test('returns 401 when the merchant no longer exists in the database', async () => {\n prismaMock.merchant.findUnique.mockResolvedValue(null);\n const req = buildReq(`Bearer ${validToken()}`);\n const res = buildRes();\n const next = jest.fn() as unknown as NextFunction;\n\n await authenticateMerchant(req, res, next);\n\n expect(res.status).toHaveBeenCalledWith(401);\n expect(res.json).toHaveBeenCalledWith({ error: 'Invalid or expired token' });\n expect(next).not.toHaveBeenCalled();\n });\n});\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\auth.services.test.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 9, + "column": 5, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 9, + "endColumn": 20 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 14, + "column": 9, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 14, + "endColumn": 17 + } + ], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 2, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import { jest, beforeEach } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\n\nconst mockVerify = { returns: true };\nconst mockKeypairError = { throws: false };\n\njest.unstable_mockModule('@stellar/stellar-sdk', () => ({\n Keypair: {\n fromPublicKey: () => {\n if (mockKeypairError.throws) {\n throw new Error('invalid public key');\n }\n return {\n verify: () => mockVerify.returns,\n };\n },\n },\n}));\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { environment } = await import('../../src/config/environment.js');\nconst {\n authenticateWallet,\n createNonce,\n verifySignature,\n buildChallengeMessage,\n issueAccessToken,\n issueRefreshToken,\n} = await import('../../src/services/auth.services.js');\n\nconst mockDate = new Date('2026-06-21T12:00:00Z');\n\ndescribe('Auth Services', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n jest.useFakeTimers({ now: mockDate });\n mockVerify.returns = true;\n mockKeypairError.throws = false;\n });\n\n afterEach(() => {\n jest.useRealTimers();\n });\n\n describe('buildChallengeMessage', () => {\n test('should construct the challenge message in a deterministic format', () => {\n const msg = buildChallengeMessage('GABCDEF123', 'nonce-abc', mockDate);\n expect(msg).toBe(\n 'Shade Authentication\\nAddress: GABCDEF123\\nNonce: nonce-abc\\nTimestamp: 2026-06-21T12:00:00.000Z',\n );\n });\n });\n\n describe('createNonce', () => {\n test('should create an AuthNonce record and return nonce, message, and expiresAt', async () => {\n const mockNonce = {\n id: 'uuid-1',\n address: 'GABCDEF123',\n nonce: 'generated-uuid',\n message:\n 'Shade Authentication\\nAddress: GABCDEF123\\nNonce: generated-uuid\\nTimestamp: 2026-06-21T12:00:00.000Z',\n expiresAt: new Date('2026-06-21T12:05:00.000Z'),\n usedAt: null,\n createdAt: mockDate,\n };\n\n prismaMock.authNonce.create.mockResolvedValue(mockNonce);\n\n const result = await createNonce('GABCDEF123');\n\n expect(result).toEqual({\n nonce: mockNonce.nonce,\n message: mockNonce.message,\n expiresAt: mockNonce.expiresAt,\n });\n expect(prismaMock.authNonce.create).toHaveBeenCalledWith({\n data: expect.objectContaining({\n address: 'GABCDEF123',\n nonce: expect.any(String),\n message: expect.any(String),\n expiresAt: expect.any(Date),\n }),\n });\n });\n });\n\n describe('verifySignature', () => {\n const address = 'GABCDEF123';\n const nonce = 'nonce-abc';\n const signature = 'deadbeef';\n const mockAuthNonce = {\n id: 'uuid-1',\n address,\n nonce,\n message: buildChallengeMessage(address, nonce, mockDate),\n expiresAt: new Date('2026-06-21T12:05:00.000Z'),\n usedAt: null,\n createdAt: mockDate,\n merchantId: null,\n };\n\n test('should return valid when signature is correct', async () => {\n prismaMock.authNonce.findUnique.mockResolvedValue(mockAuthNonce);\n\n const result = await verifySignature(address, nonce, signature);\n\n expect(result).toEqual({ valid: true, reason: null });\n expect(prismaMock.authNonce.update).toHaveBeenCalledWith({\n where: { id: 'uuid-1' },\n data: { usedAt: expect.any(Date) },\n });\n });\n\n test('should return invalid when nonce is not found', async () => {\n prismaMock.authNonce.findUnique.mockResolvedValue(null);\n\n const result = await verifySignature(address, nonce, signature);\n\n expect(result).toEqual({ valid: false, reason: 'Nonce not found' });\n });\n\n test('should return invalid when address does not match', async () => {\n prismaMock.authNonce.findUnique.mockResolvedValue(mockAuthNonce);\n\n const result = await verifySignature('GWRONG', nonce, signature);\n\n expect(result).toEqual({ valid: false, reason: 'Address mismatch' });\n });\n\n test('should return invalid when nonce is already used', async () => {\n prismaMock.authNonce.findUnique.mockResolvedValue({\n ...mockAuthNonce,\n usedAt: new Date('2026-06-21T12:01:00.000Z'),\n });\n\n const result = await verifySignature(address, nonce, signature);\n\n expect(result).toEqual({ valid: false, reason: 'Nonce already used' });\n });\n\n test('should return invalid when nonce is expired', async () => {\n jest.setSystemTime(new Date('2026-06-21T12:10:00.000Z'));\n\n prismaMock.authNonce.findUnique.mockResolvedValue(mockAuthNonce);\n\n const result = await verifySignature(address, nonce, signature);\n\n expect(result).toEqual({ valid: false, reason: 'Nonce expired' });\n });\n\n test('should return invalid when signature verification fails', async () => {\n mockVerify.returns = false;\n prismaMock.authNonce.findUnique.mockResolvedValue(mockAuthNonce);\n\n const result = await verifySignature(address, nonce, signature);\n\n expect(result).toEqual({ valid: false, reason: 'Signature verification failed' });\n });\n\n test('should return invalid when address is invalid', async () => {\n mockKeypairError.throws = true;\n prismaMock.authNonce.findUnique.mockResolvedValue(mockAuthNonce);\n\n const result = await verifySignature(address, nonce, signature);\n\n expect(result).toEqual({ valid: false, reason: 'Invalid address or signature format' });\n });\n });\n\n describe('issueAccessToken', () => {\n test('should sign a JWT with sub and address claims', async () => {\n const token = issueAccessToken('merchant-uuid', 'GABCDEF123');\n expect(typeof token).toBe('string');\n expect(token.split('.')).toHaveLength(3);\n\n const jwt = await import('jsonwebtoken');\n const decoded = jwt.default.verify(token, environment.jwtSecret);\n expect(decoded).toMatchObject({\n sub: 'merchant-uuid',\n address: 'GABCDEF123',\n });\n });\n });\n\n describe('issueRefreshToken', () => {\n test('should create a RefreshToken and return the token', async () => {\n prismaMock.refreshToken.create.mockResolvedValue({\n id: 'session-uuid',\n merchantId: 'merchant-uuid',\n token: 'ignored',\n expiresAt: new Date('2026-06-28T12:00:00.000Z'),\n createdAt: mockDate,\n });\n\n const result = await issueRefreshToken('merchant-uuid');\n\n expect(typeof result).toBe('string');\n expect(result.length).toBeGreaterThan(0);\n expect(prismaMock.refreshToken.create).toHaveBeenCalledWith({\n data: {\n merchantId: 'merchant-uuid',\n token: expect.any(String),\n expiresAt: expect.any(Date),\n },\n });\n });\n });\n\n describe('authenticateWallet', () => {\n const address = 'GABCDEF123';\n const nonce = 'nonce-abc';\n const signature = 'deadbeef';\n const mockAuthNonce = {\n id: 'uuid-1',\n address,\n nonce,\n message: buildChallengeMessage(address, nonce, mockDate),\n expiresAt: new Date('2026-06-21T12:05:00.000Z'),\n usedAt: null,\n createdAt: mockDate,\n merchantId: null,\n };\n\n test('should return tokens and merchant on successful auth (new merchant)', async () => {\n prismaMock.authNonce.findUnique.mockResolvedValue(mockAuthNonce);\n prismaMock.merchant.findFirst.mockResolvedValue(null);\n prismaMock.merchant.create.mockResolvedValue({\n id: 'merchant-uuid',\n merchantId: 123456,\n address,\n email: null,\n firstName: null,\n lastName: null,\n businessName: null,\n category: null,\n description: null,\n logo: null,\n active: true,\n verified: false,\n emailVerified: false,\n registered: false,\n createdAt: mockDate,\n updatedAt: mockDate,\n });\n prismaMock.refreshToken.create.mockResolvedValue({\n id: 'session-uuid',\n merchantId: 'merchant-uuid',\n token: 'ignored',\n expiresAt: new Date('2026-06-28T12:00:00.000Z'),\n createdAt: mockDate,\n });\n prismaMock.authNonce.update.mockResolvedValue(mockAuthNonce);\n\n const result = await authenticateWallet(address, nonce, signature);\n\n expect(result.success).toBe(true);\n if (result.success) {\n expect(result.accessToken).toBeTruthy();\n expect(typeof result.refreshToken).toBe('string');\n expect(result.refreshToken.length).toBeGreaterThan(0);\n expect(result.merchant).toEqual({\n id: 'merchant-uuid',\n address,\n isRegistered: false,\n });\n }\n });\n\n test('should return tokens and merchant on successful auth (existing merchant)', async () => {\n prismaMock.authNonce.findUnique.mockResolvedValue(mockAuthNonce);\n prismaMock.merchant.findFirst.mockResolvedValue({\n id: 'existing-merchant-uuid',\n merchantId: 654321,\n address,\n email: 'merchant@test.com',\n firstName: 'John',\n lastName: 'Doe',\n businessName: 'Acme',\n category: 'retail',\n description: 'A merchant',\n logo: null,\n active: true,\n verified: true,\n emailVerified: true,\n registered: true,\n createdAt: mockDate,\n updatedAt: mockDate,\n });\n prismaMock.refreshToken.create.mockResolvedValue({\n id: 'session-uuid',\n merchantId: 'existing-merchant-uuid',\n token: 'ignored',\n expiresAt: new Date('2026-06-28T12:00:00.000Z'),\n createdAt: mockDate,\n });\n prismaMock.authNonce.update.mockResolvedValue(mockAuthNonce);\n\n const result = await authenticateWallet(address, nonce, signature);\n\n expect(result.success).toBe(true);\n if (result.success) {\n expect(typeof result.refreshToken).toBe('string');\n expect(result.merchant).toEqual({\n id: 'existing-merchant-uuid',\n address,\n isRegistered: true,\n });\n }\n });\n\n test('should return failure when signature is invalid', async () => {\n mockVerify.returns = false;\n prismaMock.authNonce.findUnique.mockResolvedValue(mockAuthNonce);\n\n const result = await authenticateWallet(address, nonce, signature);\n\n expect(result.success).toBe(false);\n if (!result.success) {\n expect(result.reason).toBe('Signature verification failed');\n }\n });\n });\n});\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\email.service.resend.test.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\email.service.smtp.test.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\email.service.test.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\indexer.test.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/no-unused-vars", + "severity": 2, + "message": "'setCursor' is assigned a value but never used. Allowed unused vars must match /^_/u.", + "line": 18, + "column": 53, + "nodeType": null, + "messageId": "unusedVar", + "endLine": 18, + "endColumn": 62 + } + ], + "suppressedMessages": [], + "errorCount": 1, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import { jest } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\n\njest.unstable_mockModule('../../src/indexer/sorobanClient.js', () => {\n const mockServer = {\n getLatestLedger: jest.fn(),\n getEvents: jest.fn(),\n };\n return {\n __esModule: true,\n sorobanServer: mockServer,\n default: mockServer,\n };\n});\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { sorobanServer } = (await import('../../src/indexer/sorobanClient.js')) as any;\nconst { tick, startPolling, stopPolling, getCursor, setCursor, resetPoller } = await import(\n '../../src/indexer/poller.js'\n);\nconst { registerEventHandler, clearHandlers, dispatch } = await import(\n '../../src/indexer/registry.js'\n);\nconst { environment } = await import('../../src/config/environment.js');\n\ndescribe('Core Soroban Indexer Infrastructure', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n clearHandlers();\n resetPoller();\n jest.clearAllMocks();\n environment.stellar.contractId = 'C_TEST_CONTRACT_ID';\n environment.stellar.indexerStartLedger = undefined;\n prismaMock.$transaction.mockImplementation(async (cb: any) => cb(prismaMock));\n });\n\n afterEach(() => {\n stopPolling();\n });\n\n it('fails fast if STELLAR_CONTRACT_ID is unset', async () => {\n environment.stellar.contractId = '';\n await expect(tick()).rejects.toThrow(\n 'STELLAR_CONTRACT_ID environment variable is unset or empty',\n );\n await expect(startPolling()).rejects.toThrow(\n 'STELLAR_CONTRACT_ID environment variable is unset or empty',\n );\n });\n\n it('connects to RPC, fetches latest ledger, and logs decoded event without erroring', async () => {\n sorobanServer.getLatestLedger.mockResolvedValue({ sequence: 100 });\n sorobanServer.getEvents.mockResolvedValue({\n events: [\n {\n id: 'evt-1',\n topic: [],\n value: null,\n ledger: 100,\n txHash: 'hash-1',\n },\n ],\n });\n prismaMock.indexerCursor.findUnique.mockResolvedValue(null);\n prismaMock.indexerEvent.findUnique.mockResolvedValue(null);\n\n await tick();\n\n expect(sorobanServer.getLatestLedger).toHaveBeenCalled();\n expect(sorobanServer.getEvents).toHaveBeenCalledWith({\n startLedger: 100,\n filters: [{ type: 'contract', contractIds: ['C_TEST_CONTRACT_ID'] }],\n limit: 100,\n });\n expect(getCursor()).toBe(101);\n });\n\n it('persists cursor after processed batch and resumes correctly', async () => {\n prismaMock.indexerCursor.findUnique.mockResolvedValue({\n contractId: 'C_TEST_CONTRACT_ID',\n lastLedger: 50,\n });\n sorobanServer.getLatestLedger.mockResolvedValue({ sequence: 55 });\n sorobanServer.getEvents.mockResolvedValue({ events: [] });\n\n await tick();\n\n expect(sorobanServer.getEvents).toHaveBeenCalledWith({\n startLedger: 50,\n filters: [{ type: 'contract', contractIds: ['C_TEST_CONTRACT_ID'] }],\n limit: 100,\n });\n expect(prismaMock.indexerCursor.upsert).toHaveBeenCalledWith({\n where: { contractId: 'C_TEST_CONTRACT_ID' },\n update: { lastLedger: 56 },\n create: { contractId: 'C_TEST_CONTRACT_ID', lastLedger: 56 },\n });\n expect(getCursor()).toBe(56);\n });\n\n it('prevents raw event id from being dispatched twice via IndexerEvent replay guard', async () => {\n sorobanServer.getLatestLedger.mockResolvedValue({ sequence: 10 });\n sorobanServer.getEvents.mockResolvedValue({\n events: [\n {\n id: 'evt-duplicate',\n topic: [],\n value: null,\n ledger: 10,\n txHash: 'hash-dup',\n },\n ],\n });\n prismaMock.indexerCursor.findUnique.mockResolvedValue({\n contractId: 'C_TEST_CONTRACT_ID',\n lastLedger: 10,\n });\n prismaMock.indexerEvent.findUnique.mockResolvedValue({\n id: 'evt-duplicate',\n topic: '',\n ledger: 10,\n });\n\n const handler = jest.fn();\n registerEventHandler('', handler);\n\n await tick();\n\n expect(handler).not.toHaveBeenCalled();\n expect(prismaMock.indexerEvent.create).not.toHaveBeenCalled();\n });\n\n it('skips dispatching on topic with no registered handler without throwing', async () => {\n await expect(\n dispatch({\n id: 'test-id',\n topic: 'unregistered_topic',\n ledger: 1,\n txHash: 'hash',\n data: { foo: 'bar' },\n }),\n ).resolves.not.toThrow();\n });\n\n it('logs error on bad event and continues poll loop', async () => {\n sorobanServer.getLatestLedger.mockResolvedValue({ sequence: 20 });\n sorobanServer.getEvents.mockResolvedValue({\n events: [\n {\n id: 'evt-bad',\n topic: [],\n value: null,\n ledger: 20,\n txHash: 'hash-bad',\n },\n {\n id: 'evt-good',\n topic: [],\n value: null,\n ledger: 20,\n txHash: 'hash-good',\n },\n ],\n });\n prismaMock.indexerCursor.findUnique.mockResolvedValue({\n contractId: 'C_TEST_CONTRACT_ID',\n lastLedger: 20,\n });\n prismaMock.indexerEvent.findUnique.mockResolvedValue(null);\n\n const handler = jest\n .fn()\n .mockImplementationOnce(() => {\n throw new Error('Handler failed on bad event');\n })\n .mockImplementationOnce(() => {});\n registerEventHandler('', handler);\n\n await tick();\n\n expect(handler).toHaveBeenCalledTimes(2);\n expect(prismaMock.indexerEvent.create).toHaveBeenCalledTimes(1);\n expect(prismaMock.indexerEvent.create).toHaveBeenCalledWith({\n data: {\n id: 'evt-good',\n topic: '',\n ledger: 20,\n },\n });\n });\n});\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\invoice-pdf.services.test.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\invoice.schema.test.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\invoice.services.test.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\merchant.profile.services.test.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/no-unused-vars", + "severity": 2, + "message": "'jest' is defined but never used. Allowed unused vars must match /^_/u.", + "line": 1, + "column": 10, + "nodeType": null, + "messageId": "unusedVar", + "endLine": 1, + "endColumn": 14 + } + ], + "suppressedMessages": [], + "errorCount": 1, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import { jest } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { getMyProfile, updateMyProfile } = await import('../../src/services/merchant.services.js');\n\nconst baseMerchant = {\n id: 'uuid-1',\n merchantId: 1,\n address: '0x123',\n account: 'CCONTRACT',\n email: 'ada@example.com',\n firstName: 'Ada',\n lastName: 'Lovelace',\n businessName: 'Analytical Engines',\n category: 'software',\n description: 'We build computing machines.',\n logo: 'https://example.com/logo.png',\n webhook: null,\n active: true,\n verified: false,\n emailVerified: false,\n registered: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n // Internal relations that the sanitizer allow-list must strip from its output.\n refreshTokens: [{ id: 'rt-1', token: 'secret-token' }],\n apiKeys: [{ id: 'ak-1', keyHash: 'hashed-secret' }],\n};\n\ndescribe('getMyProfile', () => {\n beforeEach(() => mockReset(prismaMock));\n\n test('returns the sanitized profile including account and webhook', async () => {\n prismaMock.merchant.findUnique.mockResolvedValue(baseMerchant);\n\n const result = await getMyProfile('uuid-1');\n\n expect(prismaMock.merchant.findUnique).toHaveBeenCalledWith({ where: { id: 'uuid-1' } });\n expect(result).toMatchObject({ id: 'uuid-1', account: 'CCONTRACT', webhook: null });\n expect(result).not.toHaveProperty('refreshTokens');\n expect(result).not.toHaveProperty('apiKeys');\n });\n\n test('throws AppError(404) when the merchant does not exist', async () => {\n prismaMock.merchant.findUnique.mockResolvedValue(null);\n\n await expect(getMyProfile('missing')).rejects.toMatchObject({ statusCode: 404 });\n });\n});\n\ndescribe('updateMyProfile', () => {\n beforeEach(() => mockReset(prismaMock));\n\n test('writes only the editable fields present in the payload, trimmed', async () => {\n prismaMock.merchant.update.mockImplementation(async (args: any) => ({\n ...baseMerchant,\n ...args.data,\n }));\n\n await updateMyProfile('uuid-1', {\n firstName: ' Grace ',\n webhook: 'https://example.com/hook',\n });\n\n expect(prismaMock.merchant.update).toHaveBeenCalledWith({\n where: { id: 'uuid-1' },\n data: { firstName: 'Grace', webhook: 'https://example.com/hook' },\n });\n });\n\n test('normalizes a cleared logo/webhook to null', async () => {\n prismaMock.merchant.update.mockImplementation(async (args: any) => ({\n ...baseMerchant,\n ...args.data,\n }));\n\n await updateMyProfile('uuid-1', { logo: '', webhook: null });\n\n expect(prismaMock.merchant.update).toHaveBeenCalledWith({\n where: { id: 'uuid-1' },\n data: { logo: null, webhook: null },\n });\n });\n\n test('returns the sanitized updated profile', async () => {\n prismaMock.merchant.update.mockImplementation(async (args: any) => ({\n ...baseMerchant,\n ...args.data,\n }));\n\n const result = await updateMyProfile('uuid-1', { businessName: 'New Co' });\n\n expect(result).toMatchObject({ businessName: 'New Co' });\n expect(result).not.toHaveProperty('refreshTokens');\n });\n\n test('never writes merchantKey even if the caller smuggles it into the payload', async () => {\n prismaMock.merchant.update.mockImplementation(async (args: any) => ({\n ...baseMerchant,\n ...args.data,\n }));\n\n // A profile update must not be able to overwrite the signing key.\n await updateMyProfile('uuid-1', {\n businessName: 'New Co',\n merchantKey: 'attacker-controlled-key',\n } as any);\n\n const updateArgs = prismaMock.merchant.update.mock.calls[0][0];\n expect(updateArgs.data).not.toHaveProperty('merchantKey');\n expect(updateArgs).toEqual({ where: { id: 'uuid-1' }, data: { businessName: 'New Co' } });\n });\n});\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\merchant.register.test.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 13, + "column": 3, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 13, + "endColumn": 16 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 14, + "column": 3, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 14, + "endColumn": 18 + }, + { + "ruleId": "@typescript-eslint/explicit-function-return-type", + "severity": 1, + "message": "Missing return type on function.", + "line": 15, + "column": 3, + "nodeType": "ArrowFunctionExpression", + "messageId": "missingReturnType", + "endLine": 15, + "endColumn": 24 + } + ], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 3, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import { jest } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\n\nconst sendOtpMock = jest.fn(async () => undefined);\n\njest.unstable_mockModule('../../src/services/email.service.js', () => ({\n __esModule: true,\n sendOtp: sendOtpMock,\n}));\n\njest.unstable_mockModule('../../src/services/otp.services.js', () => ({\n __esModule: true,\n generateOtp: () => '123456',\n hashOtp: async () => 'hashed-otp',\n verifyOtpHash: async () => true,\n issueEmailOtp: jest.fn(),\n verifyEmailOtp: jest.fn(),\n resendEmailOtp: jest.fn(),\n}));\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { registerMerchant } = await import('../../src/services/merchant.services.js');\nconst { AppError } = await import('../../src/utils/errors.js');\n\nconst baseMerchant = {\n id: 'uuid-1',\n merchantId: 1,\n email: null,\n address: '0x123',\n firstName: null,\n lastName: null,\n businessName: null,\n category: null,\n description: null,\n logo: null,\n active: true,\n verified: false,\n emailVerified: false,\n registered: false,\n emailOtp: null,\n emailOtpExpiresAt: null,\n createdAt: new Date(),\n updatedAt: new Date(),\n};\n\nconst validPayload = {\n firstName: 'Ada',\n lastName: 'Lovelace',\n email: 'Ada@Example.com',\n businessName: 'Analytical Engines',\n category: 'software',\n description: 'We build computing machines.',\n};\n\ndescribe('registerMerchant service', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n sendOtpMock.mockClear();\n });\n\n test('completes registration, stores OTP hash and sends email', async () => {\n prismaMock.merchant.findUnique.mockResolvedValue(baseMerchant as any);\n prismaMock.merchant.findFirst.mockResolvedValue(null);\n prismaMock.merchant.update.mockImplementation(async (args: any) => ({\n ...baseMerchant,\n ...args.data,\n }));\n\n const result = await registerMerchant('uuid-1', validPayload);\n\n expect(prismaMock.merchant.update).toHaveBeenCalledWith({\n where: { id: 'uuid-1' },\n data: expect.objectContaining({\n firstName: 'Ada',\n lastName: 'Lovelace',\n email: 'ada@example.com',\n businessName: 'Analytical Engines',\n category: 'software',\n description: 'We build computing machines.',\n logo: null,\n emailVerified: false,\n registered: true,\n emailOtp: 'hashed-otp',\n emailOtpExpiresAt: expect.any(Date),\n }),\n });\n expect(sendOtpMock).toHaveBeenCalledWith('ada@example.com', '123456', 'Ada');\n expect(result.emailVerified).toBe(false);\n expect(result.registered).toBe(true);\n });\n\n test('throws 404 when merchant does not exist', async () => {\n prismaMock.merchant.findUnique.mockResolvedValue(null);\n\n await expect(registerMerchant('missing', validPayload)).rejects.toMatchObject({\n statusCode: 404,\n });\n expect(sendOtpMock).not.toHaveBeenCalled();\n });\n\n test('throws 409 when profile already set up', async () => {\n prismaMock.merchant.findUnique.mockResolvedValue({\n ...baseMerchant,\n registered: true,\n } as any);\n\n await expect(registerMerchant('uuid-1', validPayload)).rejects.toMatchObject({\n statusCode: 409,\n message: 'Profile already set up',\n });\n expect(prismaMock.merchant.update).not.toHaveBeenCalled();\n });\n\n test('throws 409 when email already registered by another merchant', async () => {\n prismaMock.merchant.findUnique.mockResolvedValue(baseMerchant as any);\n prismaMock.merchant.findFirst.mockResolvedValue({\n ...baseMerchant,\n id: 'uuid-2',\n email: 'ada@example.com',\n } as any);\n\n await expect(registerMerchant('uuid-1', validPayload)).rejects.toMatchObject({\n statusCode: 409,\n message: 'Email already registered',\n });\n expect(prismaMock.merchant.update).not.toHaveBeenCalled();\n });\n\n test('AppError carries the provided status code', () => {\n const err = new AppError(409, 'Email already registered');\n expect(err.statusCode).toBe(409);\n expect(err.message).toBe('Email already registered');\n });\n});\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\merchant.services.test.ts", + "messages": [ + { + "ruleId": "@typescript-eslint/no-unused-vars", + "severity": 2, + "message": "'jest' is defined but never used. Allowed unused vars must match /^_/u.", + "line": 1, + "column": 10, + "nodeType": null, + "messageId": "unusedVar", + "endLine": 1, + "endColumn": 14 + } + ], + "suppressedMessages": [], + "errorCount": 1, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "source": "import { jest } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\n\n// Wait for the mock to be applied\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { createMerchant, getMerchant, listMerchants } = await import(\n '../../src/services/merchant.services.js'\n);\n\ndescribe('Merchant Services', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n });\n\n test('should create a new merchant', async () => {\n const merchantData = {\n merchantId: 1,\n address: '0x123',\n email: 'test@example.com',\n };\n\n const expectedMerchant = {\n id: 'uuid-1',\n ...merchantData,\n active: true,\n verified: false,\n createdAt: new Date(),\n updatedAt: new Date(),\n };\n\n prismaMock.merchant.create.mockResolvedValue(expectedMerchant);\n\n const result = await createMerchant(merchantData);\n\n expect(result).toEqual(expectedMerchant);\n expect(prismaMock.merchant.create).toHaveBeenCalledWith({\n data: merchantData,\n });\n });\n\n test('should get a merchant by merchantId', async () => {\n const expectedMerchant = {\n id: 'uuid-1',\n merchantId: 1,\n address: '0x123',\n email: 'test@example.com',\n active: true,\n verified: false,\n createdAt: new Date(),\n updatedAt: new Date(),\n };\n\n prismaMock.merchant.findUnique.mockResolvedValue(expectedMerchant);\n\n const result = await getMerchant(1);\n\n expect(result).toEqual(expectedMerchant);\n expect(prismaMock.merchant.findUnique).toHaveBeenCalledWith({\n where: { merchantId: 1 },\n });\n });\n\n test('should list merchants', async () => {\n const merchants = [\n {\n id: 'uuid-1',\n merchantId: 1,\n address: '0x1',\n email: '1@test.com',\n active: true,\n verified: false,\n createdAt: new Date(),\n updatedAt: new Date(),\n },\n {\n id: 'uuid-2',\n merchantId: 2,\n address: '0x2',\n email: '2@test.com',\n active: true,\n verified: false,\n createdAt: new Date(),\n updatedAt: new Date(),\n },\n ];\n\n prismaMock.merchant.findMany.mockResolvedValue(merchants);\n\n const result = await listMerchants(10, 0);\n\n expect(result).toEqual(merchants);\n expect(prismaMock.merchant.findMany).toHaveBeenCalledWith({\n take: 10,\n skip: 0,\n });\n });\n});\n", + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\merchant.signing-key.services.test.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\merchant.update.validation.test.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\otp.services.test.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + }, + { + "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\subscription.schema.test.ts", + "messages": [], + "suppressedMessages": [], + "errorCount": 0, + "fatalErrorCount": 0, + "warningCount": 0, + "fixableErrorCount": 0, + "fixableWarningCount": 0, + "usedDeprecatedRules": [] + } +] diff --git a/eslint.config.cjs b/eslint.config.cjs index edcafd9..08c0526 100644 --- a/eslint.config.cjs +++ b/eslint.config.cjs @@ -1,3 +1,4 @@ +/* eslint-disable */ const { FlatCompat } = require('@eslint/eslintrc'); const js = require('@eslint/js'); const tseslint = require('typescript-eslint'); @@ -18,7 +19,7 @@ module.exports = [ sourceType: 'module', parser: tseslint.parser, parserOptions: { - project: './tsconfig.json', + project: './tsconfig.eslint.json', }, }, files: ['**/*.ts', '**/*.js'], @@ -33,12 +34,15 @@ module.exports = [ rules: { 'prettier/prettier': 'error', '@typescript-eslint/explicit-function-return-type': 'warn', - '@typescript-eslint/explicit-module-boundary-types': 'warn', + '@typescript-eslint/explicit-module-boundary-types': 'warn', '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-unused-vars': ['error', { - 'argsIgnorePattern': '^_', - 'varsIgnorePattern': '^_', - }], + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], 'no-console': ['warn', { allow: ['warn', 'error'] }], 'no-duplicate-imports': 'error', 'no-unused-expressions': 'error', @@ -51,5 +55,5 @@ module.exports = [ '@typescript-eslint/no-explicit-any': 'off', 'no-console': 'off', }, - } -]; \ No newline at end of file + }, +]; diff --git a/src/config/prisma.ts b/src/config/prisma.ts index 4272853..65962ae 100644 --- a/src/config/prisma.ts +++ b/src/config/prisma.ts @@ -16,6 +16,7 @@ const prismaClientSingleton = () => { }; declare global { + // eslint-disable-next-line no-var var prisma: undefined | ReturnType; } diff --git a/src/controllers/auth.controllers.ts b/src/controllers/auth.controllers.ts index 7997f29..65b4223 100644 --- a/src/controllers/auth.controllers.ts +++ b/src/controllers/auth.controllers.ts @@ -13,7 +13,7 @@ export const createNonceController = async (req: Request, res: Response) => { } const result = await createNonce(address); res.status(201).json(result); - } catch (error) { + } catch { res.status(500).json({ error: 'Internal Server Error' }); } }; @@ -42,7 +42,7 @@ export const verifySignatureController = async (req: Request, res: Response) => refreshToken: result.refreshToken, merchant: result.merchant, }); - } catch (error) { + } catch { res.status(500).json({ error: 'Internal Server Error' }); } }; diff --git a/src/controllers/merchant.controllers.ts b/src/controllers/merchant.controllers.ts index 8e41f38..864a29f 100644 --- a/src/controllers/merchant.controllers.ts +++ b/src/controllers/merchant.controllers.ts @@ -15,7 +15,7 @@ export const createMerchantController = async (req: Request, res: Response) => { try { const merchant = await createMerchant(req.body); res.status(201).json(merchant); - } catch (error) { + } catch { res.status(500).json({ error: 'Internal Server Error' }); } }; @@ -24,7 +24,7 @@ export const getMerchantController = async (req: Request, res: Response) => { try { const merchant = await getMerchant(Number(req.params.id)); res.status(200).json(merchant); - } catch (error) { + } catch { res.status(500).json({ error: 'Internal Server Error' }); } }; @@ -33,7 +33,7 @@ export const listMerchantsController = async (req: Request, res: Response) => { try { const merchants = await listMerchants(Number(req.query.limit), Number(req.query.offset)); res.status(200).json(merchants); - } catch (error) { + } catch { res.status(500).json({ error: 'Internal Server Error' }); } }; diff --git a/src/indexer/poller.ts b/src/indexer/poller.ts index db2f184..91164d4 100644 --- a/src/indexer/poller.ts +++ b/src/indexer/poller.ts @@ -103,7 +103,7 @@ export async function tick(): Promise { ? events[events.length - 1].ledger + 1 : latestLedger + 1; - await prisma.$transaction(async (tx) => { + await prisma.$transaction(async (tx: any) => { for (const item of processedIds) { await tx.indexerEvent.create({ data: { @@ -141,7 +141,7 @@ export async function startPolling(intervalMs = 6000): Promise { while (isRunning) { await tick(); if (!isRunning) break; - await new Promise((resolve) => setTimeout(resolve, intervalMs)); + await new Promise(resolve => setTimeout(resolve, intervalMs)); } } diff --git a/src/indexer/run.ts b/src/indexer/run.ts index c2d12d8..e7fad92 100644 --- a/src/indexer/run.ts +++ b/src/indexer/run.ts @@ -10,7 +10,7 @@ process.on('SIGTERM', () => { stopPolling(); }); -startPolling().catch((error) => { +startPolling().catch(error => { console.error('Fatal error starting Soroban indexer:', error); process.exit(1); }); diff --git a/src/services/api-key.services.ts b/src/services/api-key.services.ts index 919e4e6..720aa3e 100644 --- a/src/services/api-key.services.ts +++ b/src/services/api-key.services.ts @@ -44,7 +44,7 @@ export const createApiKey = async ( const { rawKey, prefix, keyHash } = generateApiKeyMaterial(); const normalizedLabel = label?.trim() || null; - const apiKey = await prisma.$transaction(async tx => { + const apiKey = await prisma.$transaction(async (tx: any) => { const activeKeys = await tx.apiKey.count({ where: activeApiKeyWhere(merchantId), }); diff --git a/src/services/merchant.services.ts b/src/services/merchant.services.ts index a7a7f30..ffafa28 100644 --- a/src/services/merchant.services.ts +++ b/src/services/merchant.services.ts @@ -43,39 +43,27 @@ export const sanitizeMerchant = (merchant: Merchant) => ({ }); export const createMerchant = async (merchantData: MerchantData) => { - try { - const merchant = await prisma.merchant.create({ - data: merchantData, - }); - return merchant; - } catch (error) { - throw error; - } + const merchant = await prisma.merchant.create({ + data: merchantData, + }); + return merchant; }; export const getMerchant = async (merchantId: number) => { - try { - const merchant = await prisma.merchant.findUnique({ - where: { - merchantId: merchantId, - }, - }); - return merchant; - } catch (error) { - throw error; - } + const merchant = await prisma.merchant.findUnique({ + where: { + merchantId: merchantId, + }, + }); + return merchant; }; export const listMerchants = async (limit: number, offset: number) => { - try { - const merchants = await prisma.merchant.findMany({ - take: limit, - skip: offset, - }); - return merchants; - } catch (error) { - throw error; - } + const merchants = await prisma.merchant.findMany({ + take: limit, + skip: offset, + }); + return merchants; }; /** diff --git a/src/services/pay.services.ts b/src/services/pay.services.ts index 8653398..5da9391 100644 --- a/src/services/pay.services.ts +++ b/src/services/pay.services.ts @@ -83,7 +83,7 @@ export const getInvoiceForPdfBySlug = async (slug: string) => { }; export const confirmPayment = async (slug: string, payerAddress: string, txHash?: string) => { - return await prisma.$transaction(async tx => { + return await prisma.$transaction(async (tx: any) => { const invoice = await tx.invoice.findUnique({ where: { paymentSlug: slug }, }); diff --git a/tests/__mocks__/prisma.ts b/tests/__mocks__/prisma.ts index 8ba06b4..8476398 100644 --- a/tests/__mocks__/prisma.ts +++ b/tests/__mocks__/prisma.ts @@ -1,14 +1,14 @@ import { jest, beforeEach } from '@jest/globals'; -import { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended'; +import { mockDeep, mockReset } from 'jest-mock-extended'; import { PrismaClient } from '@prisma/client'; export const prismaMock = mockDeep(); jest.mock('../../src/config/prisma.js', () => ({ - __esModule: true, - default: prismaMock, + __esModule: true, + default: prismaMock, })); beforeEach(() => { - mockReset(prismaMock); + mockReset(prismaMock); }); diff --git a/tests/integration/api-key.routes.test.ts b/tests/integration/api-key.routes.test.ts index 50893e1..5ede944 100644 --- a/tests/integration/api-key.routes.test.ts +++ b/tests/integration/api-key.routes.test.ts @@ -2,7 +2,6 @@ import { jest } from '@jest/globals'; import { mockReset } from 'jest-mock-extended'; import request from 'supertest'; import { - TEST_API_KEY_PREFIX, TEST_KEY_HASH, TEST_KEY_PREFIX_DISPLAY, TEST_RAW_API_KEY, diff --git a/tests/integration/auth.middleware.test.ts b/tests/integration/auth.middleware.test.ts index d221965..2ade4aa 100644 --- a/tests/integration/auth.middleware.test.ts +++ b/tests/integration/auth.middleware.test.ts @@ -1,4 +1,3 @@ -import { jest } from '@jest/globals'; import { mockReset } from 'jest-mock-extended'; import jwt from 'jsonwebtoken'; import request from 'supertest'; diff --git a/tests/integration/auth.routes.test.ts b/tests/integration/auth.routes.test.ts index 6db4a3d..2ca4b2b 100644 --- a/tests/integration/auth.routes.test.ts +++ b/tests/integration/auth.routes.test.ts @@ -18,7 +18,7 @@ jest.unstable_mockModule('@stellar/stellar-sdk', () => ({ }, })); -const { default: prismaMock } = await import('../../src/config/prisma.js') as any; +const { default: prismaMock } = (await import('../../src/config/prisma.js')) as any; const { default: app } = await import('../../src/app.js'); const address = 'GABCDEF123'; @@ -178,9 +178,7 @@ describe('Auth Routes', () => { }); test('should return 400 when required fields are missing', async () => { - const response = await request(app) - .post('/api/v1/auth/verify') - .send({}); + const response = await request(app).post('/api/v1/auth/verify').send({}); expect(response.status).toBe(400); }); diff --git a/tests/integration/merchant.profile.test.ts b/tests/integration/merchant.profile.test.ts index 4921198..690d51c 100644 --- a/tests/integration/merchant.profile.test.ts +++ b/tests/integration/merchant.profile.test.ts @@ -1,4 +1,3 @@ -import { jest } from '@jest/globals'; import { mockReset } from 'jest-mock-extended'; import request from 'supertest'; @@ -74,7 +73,10 @@ describe('PATCH /api/v1/merchants/me', () => { test('updates a valid partial payload and returns 200', async () => { authenticateAs(baseMerchant); - prismaMock.merchant.update.mockImplementation(async (args: any) => ({ ...baseMerchant, ...args.data })); + prismaMock.merchant.update.mockImplementation(async (args: any) => ({ + ...baseMerchant, + ...args.data, + })); const response = await request(app) .patch(ME_URL) @@ -82,12 +84,18 @@ describe('PATCH /api/v1/merchants/me', () => { .send({ firstName: 'Grace', webhook: 'https://example.com/hook' }); expect(response.status).toBe(200); - expect(response.body).toMatchObject({ firstName: 'Grace', webhook: 'https://example.com/hook' }); + expect(response.body).toMatchObject({ + firstName: 'Grace', + webhook: 'https://example.com/hook', + }); }); test('silently ignores non-editable fields (address/email/merchantId/account)', async () => { authenticateAs(baseMerchant); - prismaMock.merchant.update.mockImplementation(async (args: any) => ({ ...baseMerchant, ...args.data })); + prismaMock.merchant.update.mockImplementation(async (args: any) => ({ + ...baseMerchant, + ...args.data, + })); const response = await request(app) .patch(ME_URL) @@ -124,7 +132,10 @@ describe('PATCH /api/v1/merchants/me', () => { test('clears the webhook when sent null', async () => { authenticateAs(baseMerchant); - prismaMock.merchant.update.mockImplementation(async (args: any) => ({ ...baseMerchant, ...args.data })); + prismaMock.merchant.update.mockImplementation(async (args: any) => ({ + ...baseMerchant, + ...args.data, + })); const response = await request(app) .patch(ME_URL) diff --git a/tests/integration/merchant.routes.test.ts b/tests/integration/merchant.routes.test.ts index ea3f4ac..e02b245 100644 --- a/tests/integration/merchant.routes.test.ts +++ b/tests/integration/merchant.routes.test.ts @@ -1,72 +1,78 @@ -import { jest } from '@jest/globals'; import { mockReset } from 'jest-mock-extended'; import request from 'supertest'; // Wait for the mock to be applied -const { default: prismaMock } = await import('../../src/config/prisma.js') as any; +const { default: prismaMock } = (await import('../../src/config/prisma.js')) as any; const { default: app } = await import('../../src/app.js'); describe('Merchant Routes', () => { - beforeEach(() => { - mockReset(prismaMock); - }); + beforeEach(() => { + mockReset(prismaMock); + }); - test('POST /api/v1/merchants should create a merchant', async () => { - const merchantData = { - merchantId: 1, - address: '0x123', - email: 'test@example.com' - }; + test('POST /api/v1/merchants should create a merchant', async () => { + const merchantData = { + merchantId: 1, + address: '0x123', + email: 'test@example.com', + }; - const expectedMerchant = { - id: 'uuid-1', - ...merchantData, - active: true, - verified: false, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() - }; + const expectedMerchant = { + id: 'uuid-1', + ...merchantData, + active: true, + verified: false, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; - prismaMock.merchant.create.mockResolvedValue(expectedMerchant as any); + prismaMock.merchant.create.mockResolvedValue(expectedMerchant as any); - const response = await request(app) - .post('/api/v1/merchants') - .send(merchantData); + const response = await request(app).post('/api/v1/merchants').send(merchantData); - expect(response.status).toBe(201); - expect(response.body).toEqual(expectedMerchant); - }); + expect(response.status).toBe(201); + expect(response.body).toEqual(expectedMerchant); + }); - test('GET /api/v1/merchants/:id should return a merchant', async () => { - const expectedMerchant = { - id: 'uuid-1', - merchantId: 1, - address: '0x123', - email: 'test@example.com', - active: true, - verified: false, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() - }; + test('GET /api/v1/merchants/:id should return a merchant', async () => { + const expectedMerchant = { + id: 'uuid-1', + merchantId: 1, + address: '0x123', + email: 'test@example.com', + active: true, + verified: false, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; - prismaMock.merchant.findUnique.mockResolvedValue(expectedMerchant as any); + prismaMock.merchant.findUnique.mockResolvedValue(expectedMerchant as any); - const response = await request(app).get('/api/v1/merchants/1'); + const response = await request(app).get('/api/v1/merchants/1'); - expect(response.status).toBe(200); - expect(response.body).toEqual(expectedMerchant); - }); + expect(response.status).toBe(200); + expect(response.body).toEqual(expectedMerchant); + }); - test('GET /api/v1/merchants should list merchants', async () => { - const merchants = [ - { id: 'uuid-1', merchantId: 1, address: '0x1', email: '1', active: true, verified: false, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } - ]; + test('GET /api/v1/merchants should list merchants', async () => { + const merchants = [ + { + id: 'uuid-1', + merchantId: 1, + address: '0x1', + email: '1', + active: true, + verified: false, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + ]; - prismaMock.merchant.findMany.mockResolvedValue(merchants as any); + prismaMock.merchant.findMany.mockResolvedValue(merchants as any); - const response = await request(app).get('/api/v1/merchants?limit=10&offset=0'); + const response = await request(app).get('/api/v1/merchants?limit=10&offset=0'); - expect(response.status).toBe(200); - expect(response.body).toEqual(merchants); - }); + expect(response.status).toBe(200); + expect(response.body).toEqual(merchants); + }); }); diff --git a/tests/jest.setup.ts b/tests/jest.setup.ts index b93c322..54649db 100644 --- a/tests/jest.setup.ts +++ b/tests/jest.setup.ts @@ -3,10 +3,10 @@ import { mockDeep } from 'jest-mock-extended'; // This is the most robust way to mock in Jest ESM jest.unstable_mockModule('../src/config/prisma.js', () => { - return { - __esModule: true, - default: mockDeep(), - }; + return { + __esModule: true, + default: mockDeep(), + }; }); process.env.DATABASE_URL = 'postgresql://postgres:postgres@localhost:5432/postgres?schema=public'; diff --git a/tests/unit/auth.services.test.ts b/tests/unit/auth.services.test.ts index 145760e..3310385 100644 --- a/tests/unit/auth.services.test.ts +++ b/tests/unit/auth.services.test.ts @@ -17,7 +17,7 @@ jest.unstable_mockModule('@stellar/stellar-sdk', () => ({ }, })); -const { default: prismaMock } = await import('../../src/config/prisma.js') as any; +const { default: prismaMock } = (await import('../../src/config/prisma.js')) as any; const { environment } = await import('../../src/config/environment.js'); const { authenticateWallet, @@ -57,7 +57,8 @@ describe('Auth Services', () => { id: 'uuid-1', address: 'GABCDEF123', nonce: 'generated-uuid', - message: 'Shade Authentication\nAddress: GABCDEF123\nNonce: generated-uuid\nTimestamp: 2026-06-21T12:00:00.000Z', + message: + 'Shade Authentication\nAddress: GABCDEF123\nNonce: generated-uuid\nTimestamp: 2026-06-21T12:00:00.000Z', expiresAt: new Date('2026-06-21T12:05:00.000Z'), usedAt: null, createdAt: mockDate, diff --git a/tests/unit/indexer.test.ts b/tests/unit/indexer.test.ts index d1f8d46..d92bfda 100644 --- a/tests/unit/indexer.test.ts +++ b/tests/unit/indexer.test.ts @@ -15,7 +15,7 @@ jest.unstable_mockModule('../../src/indexer/sorobanClient.js', () => { const { default: prismaMock } = (await import('../../src/config/prisma.js')) as any; const { sorobanServer } = (await import('../../src/indexer/sorobanClient.js')) as any; -const { tick, startPolling, stopPolling, getCursor, setCursor, resetPoller } = await import( +const { tick, startPolling, stopPolling, getCursor, resetPoller } = await import( '../../src/indexer/poller.js' ); const { registerEventHandler, clearHandlers, dispatch } = await import( @@ -40,8 +40,12 @@ describe('Core Soroban Indexer Infrastructure', () => { it('fails fast if STELLAR_CONTRACT_ID is unset', async () => { environment.stellar.contractId = ''; - await expect(tick()).rejects.toThrow('STELLAR_CONTRACT_ID environment variable is unset or empty'); - await expect(startPolling()).rejects.toThrow('STELLAR_CONTRACT_ID environment variable is unset or empty'); + await expect(tick()).rejects.toThrow( + 'STELLAR_CONTRACT_ID environment variable is unset or empty', + ); + await expect(startPolling()).rejects.toThrow( + 'STELLAR_CONTRACT_ID environment variable is unset or empty', + ); }); it('connects to RPC, fetches latest ledger, and logs decoded event without erroring', async () => { @@ -72,7 +76,10 @@ describe('Core Soroban Indexer Infrastructure', () => { }); it('persists cursor after processed batch and resumes correctly', async () => { - prismaMock.indexerCursor.findUnique.mockResolvedValue({ contractId: 'C_TEST_CONTRACT_ID', lastLedger: 50 }); + prismaMock.indexerCursor.findUnique.mockResolvedValue({ + contractId: 'C_TEST_CONTRACT_ID', + lastLedger: 50, + }); sorobanServer.getLatestLedger.mockResolvedValue({ sequence: 55 }); sorobanServer.getEvents.mockResolvedValue({ events: [] }); @@ -104,8 +111,15 @@ describe('Core Soroban Indexer Infrastructure', () => { }, ], }); - prismaMock.indexerCursor.findUnique.mockResolvedValue({ contractId: 'C_TEST_CONTRACT_ID', lastLedger: 10 }); - prismaMock.indexerEvent.findUnique.mockResolvedValue({ id: 'evt-duplicate', topic: '', ledger: 10 }); + prismaMock.indexerCursor.findUnique.mockResolvedValue({ + contractId: 'C_TEST_CONTRACT_ID', + lastLedger: 10, + }); + prismaMock.indexerEvent.findUnique.mockResolvedValue({ + id: 'evt-duplicate', + topic: '', + ledger: 10, + }); const handler = jest.fn(); registerEventHandler('', handler); @@ -124,7 +138,7 @@ describe('Core Soroban Indexer Infrastructure', () => { ledger: 1, txHash: 'hash', data: { foo: 'bar' }, - }) + }), ).resolves.not.toThrow(); }); @@ -148,12 +162,18 @@ describe('Core Soroban Indexer Infrastructure', () => { }, ], }); - prismaMock.indexerCursor.findUnique.mockResolvedValue({ contractId: 'C_TEST_CONTRACT_ID', lastLedger: 20 }); + prismaMock.indexerCursor.findUnique.mockResolvedValue({ + contractId: 'C_TEST_CONTRACT_ID', + lastLedger: 20, + }); prismaMock.indexerEvent.findUnique.mockResolvedValue(null); - const handler = jest.fn().mockImplementationOnce(() => { - throw new Error('Handler failed on bad event'); - }).mockImplementationOnce(() => {}); + const handler = jest + .fn() + .mockImplementationOnce(() => { + throw new Error('Handler failed on bad event'); + }) + .mockImplementationOnce(() => {}); registerEventHandler('', handler); await tick(); diff --git a/tests/unit/invoice.schema.test.ts b/tests/unit/invoice.schema.test.ts index c2305b0..b961124 100644 --- a/tests/unit/invoice.schema.test.ts +++ b/tests/unit/invoice.schema.test.ts @@ -159,7 +159,7 @@ describe('Invoice schema', () => { [InvoiceStatus.PARTIALLY_REFUNDED], [InvoiceStatus.PARTIALLY_PAID], [InvoiceStatus.DRAFT], - ])('accepts status %s', async (status) => { + ])('accepts status %s', async status => { prismaMock.invoice.update.mockResolvedValue({ ...baseInvoice, status }); const result = await prismaMock.invoice.update({ diff --git a/tests/unit/merchant.profile.services.test.ts b/tests/unit/merchant.profile.services.test.ts index 7ab9f67..b4fa43b 100644 --- a/tests/unit/merchant.profile.services.test.ts +++ b/tests/unit/merchant.profile.services.test.ts @@ -1,4 +1,3 @@ -import { jest } from '@jest/globals'; import { mockReset } from 'jest-mock-extended'; const { default: prismaMock } = (await import('../../src/config/prisma.js')) as any; diff --git a/tests/unit/merchant.services.test.ts b/tests/unit/merchant.services.test.ts index 2ab9489..8c4a006 100644 --- a/tests/unit/merchant.services.test.ts +++ b/tests/unit/merchant.services.test.ts @@ -1,77 +1,96 @@ -import { jest } from '@jest/globals'; import { mockReset } from 'jest-mock-extended'; // Wait for the mock to be applied -const { default: prismaMock } = await import('../../src/config/prisma.js') as any; -const { createMerchant, getMerchant, listMerchants } = await import('../../src/services/merchant.services.js'); +const { default: prismaMock } = (await import('../../src/config/prisma.js')) as any; +const { createMerchant, getMerchant, listMerchants } = await import( + '../../src/services/merchant.services.js' +); describe('Merchant Services', () => { - beforeEach(() => { - mockReset(prismaMock); - }); + beforeEach(() => { + mockReset(prismaMock); + }); - test('should create a new merchant', async () => { - const merchantData = { - merchantId: 1, - address: '0x123', - email: 'test@example.com' - }; + test('should create a new merchant', async () => { + const merchantData = { + merchantId: 1, + address: '0x123', + email: 'test@example.com', + }; - const expectedMerchant = { - id: 'uuid-1', - ...merchantData, - active: true, - verified: false, - createdAt: new Date(), - updatedAt: new Date() - }; + const expectedMerchant = { + id: 'uuid-1', + ...merchantData, + active: true, + verified: false, + createdAt: new Date(), + updatedAt: new Date(), + }; - prismaMock.merchant.create.mockResolvedValue(expectedMerchant); + prismaMock.merchant.create.mockResolvedValue(expectedMerchant); - const result = await createMerchant(merchantData); + const result = await createMerchant(merchantData); - expect(result).toEqual(expectedMerchant); - expect(prismaMock.merchant.create).toHaveBeenCalledWith({ - data: merchantData, - }); + expect(result).toEqual(expectedMerchant); + expect(prismaMock.merchant.create).toHaveBeenCalledWith({ + data: merchantData, }); + }); - test('should get a merchant by merchantId', async () => { - const expectedMerchant = { - id: 'uuid-1', - merchantId: 1, - address: '0x123', - email: 'test@example.com', - active: true, - verified: false, - createdAt: new Date(), - updatedAt: new Date() - }; + test('should get a merchant by merchantId', async () => { + const expectedMerchant = { + id: 'uuid-1', + merchantId: 1, + address: '0x123', + email: 'test@example.com', + active: true, + verified: false, + createdAt: new Date(), + updatedAt: new Date(), + }; - prismaMock.merchant.findUnique.mockResolvedValue(expectedMerchant); + prismaMock.merchant.findUnique.mockResolvedValue(expectedMerchant); - const result = await getMerchant(1); + const result = await getMerchant(1); - expect(result).toEqual(expectedMerchant); - expect(prismaMock.merchant.findUnique).toHaveBeenCalledWith({ - where: { merchantId: 1 }, - }); + expect(result).toEqual(expectedMerchant); + expect(prismaMock.merchant.findUnique).toHaveBeenCalledWith({ + where: { merchantId: 1 }, }); + }); - test('should list merchants', async () => { - const merchants = [ - { id: 'uuid-1', merchantId: 1, address: '0x1', email: '1@test.com', active: true, verified: false, createdAt: new Date(), updatedAt: new Date() }, - { id: 'uuid-2', merchantId: 2, address: '0x2', email: '2@test.com', active: true, verified: false, createdAt: new Date(), updatedAt: new Date() } - ]; + test('should list merchants', async () => { + const merchants = [ + { + id: 'uuid-1', + merchantId: 1, + address: '0x1', + email: '1@test.com', + active: true, + verified: false, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: 'uuid-2', + merchantId: 2, + address: '0x2', + email: '2@test.com', + active: true, + verified: false, + createdAt: new Date(), + updatedAt: new Date(), + }, + ]; - prismaMock.merchant.findMany.mockResolvedValue(merchants); + prismaMock.merchant.findMany.mockResolvedValue(merchants); - const result = await listMerchants(10, 0); + const result = await listMerchants(10, 0); - expect(result).toEqual(merchants); - expect(prismaMock.merchant.findMany).toHaveBeenCalledWith({ - take: 10, - skip: 0, - }); + expect(result).toEqual(merchants); + expect(prismaMock.merchant.findMany).toHaveBeenCalledWith({ + take: 10, + skip: 0, }); + }); }); diff --git a/tests/unit/subscription.schema.test.ts b/tests/unit/subscription.schema.test.ts index a083b25..366ec7d 100644 --- a/tests/unit/subscription.schema.test.ts +++ b/tests/unit/subscription.schema.test.ts @@ -172,19 +172,19 @@ describe('Subscription schema', () => { expect(result.status).toBe(SubscriptionStatus.CANCELLED); }); - test.each([ - [SubscriptionStatus.ACTIVE], - [SubscriptionStatus.CANCELLED], - ])('accepts status %s', async (status) => { - prismaMock.subscription.update.mockResolvedValue({ ...baseSubscription, status }); - - const result = await prismaMock.subscription.update({ - where: { id: 'sub-uuid' }, - data: { status }, - }); - - expect(result.status).toBe(status); - }); + test.each([[SubscriptionStatus.ACTIVE], [SubscriptionStatus.CANCELLED]])( + 'accepts status %s', + async status => { + prismaMock.subscription.update.mockResolvedValue({ ...baseSubscription, status }); + + const result = await prismaMock.subscription.update({ + where: { id: 'sub-uuid' }, + data: { status }, + }); + + expect(result.status).toBe(status); + }, + ); }); describe('unique constraints', () => { diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 0000000..47c13f3 --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["**/*.ts", "**/*.js", "tests/**/*.ts", "tests/**/*.js"], + "exclude": ["node_modules", "dist"] +} diff --git a/tsconfig.json b/tsconfig.json index 0000995..182e7e6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,15 +6,7 @@ "strict": true, "isolatedModules": true, "esModuleInterop": true, - "types": [ - "jest", - "node" - ] + "types": ["jest", "node"] }, - "exclude": [ - "node_modules", - "dist", - "tests", - "jest.config.ts" - ] -} \ No newline at end of file + "exclude": ["node_modules", "dist", "tests", "jest.config.ts"] +} From 03143b47e99a3e3e26a01de7a9ecab42f9ad7be3 Mon Sep 17 00:00:00 2001 From: CodeBestia <158113179+codebestia@users.noreply.github.com> Date: Wed, 29 Jul 2026 02:23:47 +0100 Subject: [PATCH 7/7] Delete eslint-report.json --- eslint-report.json | 2419 -------------------------------------------- 1 file changed, 2419 deletions(-) delete mode 100644 eslint-report.json diff --git a/eslint-report.json b/eslint-report.json deleted file mode 100644 index cde5d94..0000000 --- a/eslint-report.json +++ /dev/null @@ -1,2419 +0,0 @@ -[ - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\eslint.config.cjs", - "messages": [ - { - "ruleId": "@typescript-eslint/no-require-imports", - "severity": 2, - "message": "A `require()` style import is forbidden.", - "line": 1, - "column": 24, - "nodeType": "CallExpression", - "messageId": "noRequireImports", - "endLine": 1, - "endColumn": 51 - }, - { - "ruleId": "no-undef", - "severity": 2, - "message": "'require' is not defined.", - "line": 1, - "column": 24, - "nodeType": "Identifier", - "messageId": "undef", - "endLine": 1, - "endColumn": 31 - }, - { - "ruleId": "@typescript-eslint/no-require-imports", - "severity": 2, - "message": "A `require()` style import is forbidden.", - "line": 2, - "column": 12, - "nodeType": "CallExpression", - "messageId": "noRequireImports", - "endLine": 2, - "endColumn": 33 - }, - { - "ruleId": "no-undef", - "severity": 2, - "message": "'require' is not defined.", - "line": 2, - "column": 12, - "nodeType": "Identifier", - "messageId": "undef", - "endLine": 2, - "endColumn": 19 - }, - { - "ruleId": "@typescript-eslint/no-require-imports", - "severity": 2, - "message": "A `require()` style import is forbidden.", - "line": 3, - "column": 18, - "nodeType": "CallExpression", - "messageId": "noRequireImports", - "endLine": 3, - "endColumn": 46 - }, - { - "ruleId": "no-undef", - "severity": 2, - "message": "'require' is not defined.", - "line": 3, - "column": 18, - "nodeType": "Identifier", - "messageId": "undef", - "endLine": 3, - "endColumn": 25 - }, - { - "ruleId": "no-undef", - "severity": 2, - "message": "'module' is not defined.", - "line": 8, - "column": 1, - "nodeType": "Identifier", - "messageId": "undef", - "endLine": 8, - "endColumn": 7 - } - ], - "suppressedMessages": [], - "errorCount": 7, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "const { FlatCompat } = require('@eslint/eslintrc');\nconst js = require('@eslint/js');\nconst tseslint = require('typescript-eslint');\n\n// Create compatibility layer between new flat config and traditional config formats\nconst compat = new FlatCompat();\n\nmodule.exports = [\n js.configs.recommended,\n ...tseslint.configs.recommended,\n ...compat.config({\n extends: ['prettier'],\n plugins: ['prettier'],\n }),\n {\n languageOptions: {\n ecmaVersion: 'latest',\n sourceType: 'module',\n parser: tseslint.parser,\n parserOptions: {\n project: './tsconfig.eslint.json',\n },\n },\n files: ['**/*.ts', '**/*.js'],\n ignores: [\n 'node_modules/**',\n 'dist/**',\n 'build/**',\n 'coverage/**',\n '**/*.d.ts',\n 'eslint.config.cjs',\n ],\n rules: {\n 'prettier/prettier': 'error',\n '@typescript-eslint/explicit-function-return-type': 'warn',\n '@typescript-eslint/explicit-module-boundary-types': 'warn', \n '@typescript-eslint/no-explicit-any': 'warn',\n '@typescript-eslint/no-unused-vars': ['error', { \n 'argsIgnorePattern': '^_',\n 'varsIgnorePattern': '^_',\n }],\n 'no-console': ['warn', { allow: ['warn', 'error'] }],\n 'no-duplicate-imports': 'error',\n 'no-unused-expressions': 'error',\n 'prefer-const': 'error',\n },\n },\n {\n files: ['**/*.test.ts', '**/*.spec.ts', 'tests/**/*.ts'],\n rules: {\n '@typescript-eslint/no-explicit-any': 'off',\n 'no-console': 'off',\n },\n }\n];", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\jest.config.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\prisma.config.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\app.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\config\\database.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\config\\environment.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\config\\prisma.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 12, - "column": 34, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 12, - "endColumn": 36 - }, - { - "ruleId": "no-var", - "severity": 2, - "message": "Unexpected var, use let or const instead.", - "line": 19, - "column": 3, - "nodeType": "VariableDeclaration", - "messageId": "unexpectedVar", - "endLine": 19, - "endColumn": 68 - } - ], - "suppressedMessages": [], - "errorCount": 1, - "fatalErrorCount": 0, - "warningCount": 1, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import dotenv from 'dotenv';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { PrismaClient } from '@prisma/client';\nimport { PrismaPg } from '@prisma/adapter-pg';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\ndotenv.config({ path: path.join(__dirname, '../../.env') });\n\nconst prismaClientSingleton = () => {\n if (process.env.NODE_ENV === 'test') return {} as PrismaClient;\n const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! });\n return new PrismaClient({ adapter });\n};\n\ndeclare global {\n var prisma: undefined | ReturnType;\n}\n\nconst prisma = globalThis.prisma ?? prismaClientSingleton();\n\nexport default prisma;\n\nif (process.env.NODE_ENV !== 'production') globalThis.prisma = prisma;\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\controllers\\api-key.controllers.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\controllers\\auth.controllers.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 7, - "column": 74, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 7, - "endColumn": 76 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 7, - "column": 74, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 7, - "endColumn": 76 - }, - { - "ruleId": "@typescript-eslint/no-unused-vars", - "severity": 2, - "message": "'error' is defined but never used.", - "line": 16, - "column": 12, - "nodeType": null, - "messageId": "unusedVar", - "endLine": 16, - "endColumn": 17 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 21, - "column": 78, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 21, - "endColumn": 80 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 21, - "column": 78, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 21, - "endColumn": 80 - }, - { - "ruleId": "@typescript-eslint/no-unused-vars", - "severity": 2, - "message": "'error' is defined but never used.", - "line": 45, - "column": 12, - "nodeType": null, - "messageId": "unusedVar", - "endLine": 45, - "endColumn": 17 - } - ], - "suppressedMessages": [], - "errorCount": 2, - "fatalErrorCount": 0, - "warningCount": 4, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import { Request, Response } from 'express';\nimport { createNonce, authenticateWallet } from '../services/auth.services.js';\nimport { resendEmailOtp, verifyEmailOtp } from '../services/otp.services.js';\nimport { sanitizeMerchant } from '../services/merchant.services.js';\nimport { AppError } from '../utils/errors.js';\n\nexport const createNonceController = async (req: Request, res: Response) => {\n try {\n const { address } = req.body;\n if (!address || typeof address !== 'string') {\n res.status(400).json({ error: 'address is required' });\n return;\n }\n const result = await createNonce(address);\n res.status(201).json(result);\n } catch (error) {\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n\nexport const verifySignatureController = async (req: Request, res: Response) => {\n try {\n const { address, nonce, signature } = req.body;\n if (!address || !nonce || !signature) {\n res.status(400).json({ error: 'address, nonce, and signature are required' });\n return;\n }\n if (typeof address !== 'string' || typeof nonce !== 'string' || typeof signature !== 'string') {\n res.status(400).json({ error: 'address, nonce, and signature must be strings' });\n return;\n }\n\n const result = await authenticateWallet(address, nonce, signature);\n\n if (!result.success) {\n res.status(401).json({ error: result.reason });\n return;\n }\n\n res.status(200).json({\n accessToken: result.accessToken,\n refreshToken: result.refreshToken,\n merchant: result.merchant,\n });\n } catch (error) {\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n\nexport const verifyEmailController = async (req: Request, res: Response): Promise => {\n const merchant = req.merchant;\n\n if (!merchant) {\n res.status(401).json({ error: 'Unauthorized' });\n return;\n }\n\n const { code } = req.body;\n if (!code || typeof code !== 'string') {\n res.status(400).json({ error: 'code is required' });\n return;\n }\n\n try {\n const updatedMerchant = await verifyEmailOtp(merchant.id, code.trim());\n res.status(200).json(sanitizeMerchant(updatedMerchant));\n } catch (error) {\n if (error instanceof AppError) {\n res.status(error.statusCode).json({ error: error.message });\n return;\n }\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n\nexport const resendOtpController = async (req: Request, res: Response): Promise => {\n const merchant = req.merchant;\n\n if (!merchant) {\n res.status(401).json({ error: 'Unauthorized' });\n return;\n }\n\n try {\n await resendEmailOtp(merchant.id);\n res.status(200).json({ message: 'Verification code sent' });\n } catch (error) {\n if (error instanceof AppError) {\n res.status(error.statusCode).json({ error: error.message });\n return;\n }\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\controllers\\index.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\controllers\\invoice.controllers.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\controllers\\merchant.controllers.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 14, - "column": 77, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 14, - "endColumn": 79 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 14, - "column": 77, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 14, - "endColumn": 79 - }, - { - "ruleId": "@typescript-eslint/no-unused-vars", - "severity": 2, - "message": "'error' is defined but never used.", - "line": 18, - "column": 12, - "nodeType": null, - "messageId": "unusedVar", - "endLine": 18, - "endColumn": 17 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 23, - "column": 74, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 23, - "endColumn": 76 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 23, - "column": 74, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 23, - "endColumn": 76 - }, - { - "ruleId": "@typescript-eslint/no-unused-vars", - "severity": 2, - "message": "'error' is defined but never used.", - "line": 27, - "column": 12, - "nodeType": null, - "messageId": "unusedVar", - "endLine": 27, - "endColumn": 17 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 32, - "column": 76, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 32, - "endColumn": 78 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 32, - "column": 76, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 32, - "endColumn": 78 - }, - { - "ruleId": "@typescript-eslint/no-unused-vars", - "severity": 2, - "message": "'error' is defined but never used.", - "line": 36, - "column": 12, - "nodeType": null, - "messageId": "unusedVar", - "endLine": 36, - "endColumn": 17 - } - ], - "suppressedMessages": [], - "errorCount": 3, - "fatalErrorCount": 0, - "warningCount": 6, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import { Request, Response } from 'express';\nimport {\n createMerchant,\n getMerchant,\n listMerchants,\n registerMerchant,\n getMyProfile,\n updateMyProfile,\n generateMerchantSigningKey,\n} from '../services/merchant.services.js';\nimport { validateRegisterMerchant, validateUpdateMerchant } from '../utils/validation.js';\nimport { AppError } from '../utils/errors.js';\n\nexport const createMerchantController = async (req: Request, res: Response) => {\n try {\n const merchant = await createMerchant(req.body);\n res.status(201).json(merchant);\n } catch (error) {\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n\nexport const getMerchantController = async (req: Request, res: Response) => {\n try {\n const merchant = await getMerchant(Number(req.params.id));\n res.status(200).json(merchant);\n } catch (error) {\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n\nexport const listMerchantsController = async (req: Request, res: Response) => {\n try {\n const merchants = await listMerchants(Number(req.query.limit), Number(req.query.offset));\n res.status(200).json(merchants);\n } catch (error) {\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n\nexport const registerMerchantController = async (req: Request, res: Response): Promise => {\n const merchant = req.merchant;\n\n if (!merchant) {\n res.status(401).json({ error: 'Unauthorized' });\n return;\n }\n\n const errors = validateRegisterMerchant(req.body);\n if (Object.keys(errors).length > 0) {\n res.status(400).json({ error: 'Validation failed', errors });\n return;\n }\n\n try {\n const profile = await registerMerchant(merchant.id, req.body);\n res.status(200).json(profile);\n } catch (error) {\n if (error instanceof AppError) {\n res.status(error.statusCode).json({ error: error.message });\n return;\n }\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n\nexport const getMyProfileController = async (req: Request, res: Response): Promise => {\n const merchant = req.merchant;\n\n if (!merchant) {\n res.status(401).json({ error: 'Unauthorized' });\n return;\n }\n\n try {\n const profile = await getMyProfile(merchant.id);\n res.status(200).json(profile);\n } catch (error) {\n if (error instanceof AppError) {\n res.status(error.statusCode).json({ error: error.message });\n return;\n }\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n\nexport const generateSigningKeyController = async (req: Request, res: Response): Promise => {\n const merchant = req.merchant;\n\n if (!merchant) {\n res.status(401).json({ error: 'Unauthorized' });\n return;\n }\n\n try {\n const keys = await generateMerchantSigningKey(merchant.id);\n res.status(201).json(keys);\n } catch (error) {\n if (error instanceof AppError) {\n res.status(error.statusCode).json({ error: error.message });\n return;\n }\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n\nexport const updateMyProfileController = async (req: Request, res: Response): Promise => {\n const merchant = req.merchant;\n\n if (!merchant) {\n res.status(401).json({ error: 'Unauthorized' });\n return;\n }\n\n const errors = validateUpdateMerchant(req.body);\n if (Object.keys(errors).length > 0) {\n res.status(400).json({ error: 'Validation failed', errors });\n return;\n }\n\n try {\n const profile = await updateMyProfile(merchant.id, req.body);\n res.status(200).json(profile);\n } catch (error) {\n if (error instanceof AppError) {\n res.status(error.statusCode).json({ error: error.message });\n return;\n }\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\controllers\\pay.controllers.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\entities\\index.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\indexer\\handlers\\index.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\indexer\\poller.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/no-explicit-any", - "severity": 1, - "message": "Unexpected any. Specify a different type.", - "line": 10, - "column": 27, - "nodeType": "TSAnyKeyword", - "messageId": "unexpectedAny", - "endLine": 10, - "endColumn": 30, - "suggestions": [ - { - "messageId": "suggestUnknown", - "fix": { "range": [329, 332], "text": "unknown" }, - "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." - }, - { - "messageId": "suggestNever", - "fix": { "range": [329, 332], "text": "never" }, - "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." - } - ] - }, - { - "ruleId": "no-console", - "severity": 1, - "message": "Unexpected console statement. Only these console methods are allowed: warn, error.", - "line": 44, - "column": 7, - "nodeType": "MemberExpression", - "messageId": "limited", - "endLine": 44, - "endColumn": 18, - "suggestions": [ - { - "fix": { "range": [1371, 1438], "text": "" }, - "messageId": "removeConsole", - "data": { "propertyName": "log" }, - "desc": "Remove the console.log()." - } - ] - }, - { - "ruleId": "@typescript-eslint/no-explicit-any", - "severity": 1, - "message": "Unexpected any. Specify a different type.", - "line": 74, - "column": 27, - "nodeType": "TSAnyKeyword", - "messageId": "unexpectedAny", - "endLine": 74, - "endColumn": 30, - "suggestions": [ - { - "messageId": "suggestUnknown", - "fix": { "range": [2274, 2277], "text": "unknown" }, - "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." - }, - { - "messageId": "suggestNever", - "fix": { "range": [2274, 2277], "text": "never" }, - "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." - } - ] - }, - { - "ruleId": "no-console", - "severity": 1, - "message": "Unexpected console statement. Only these console methods are allowed: warn, error.", - "line": 81, - "column": 9, - "nodeType": "MemberExpression", - "messageId": "limited", - "endLine": 81, - "endColumn": 20, - "suggestions": [ - { - "fix": { "range": [2442, 2532], "text": "" }, - "messageId": "removeConsole", - "data": { "propertyName": "log" }, - "desc": "Remove the console.log()." - } - ] - }, - { - "ruleId": "no-console", - "severity": 1, - "message": "Unexpected console statement. Only these console methods are allowed: warn, error.", - "line": 139, - "column": 3, - "nodeType": "MemberExpression", - "messageId": "limited", - "endLine": 139, - "endColumn": 14, - "suggestions": [ - { - "fix": { "range": [4086, 4163], "text": "" }, - "messageId": "removeConsole", - "data": { "propertyName": "log" }, - "desc": "Remove the console.log()." - } - ] - } - ], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 5, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import { scValToNative } from '@stellar/stellar-sdk';\nimport prisma from '../config/prisma.js';\nimport { environment } from '../config/environment.js';\nimport { sorobanServer } from './sorobanClient.js';\nimport { dispatch } from './registry.js';\n\nlet isRunning = false;\nlet cursor: number | undefined;\n\nfunction decodeTopic(val: any): string {\n if (!val) return '';\n try {\n const native = scValToNative(val);\n if (typeof native === 'symbol') {\n return native.description ?? native.toString();\n }\n return String(native);\n } catch {\n return 'unknown_topic';\n }\n}\n\nexport async function tick(): Promise {\n try {\n const contractId = environment.stellar.contractId;\n if (!contractId || contractId.trim() === '') {\n throw new Error('STELLAR_CONTRACT_ID environment variable is unset or empty');\n }\n\n const latestLedgerResp = await sorobanServer.getLatestLedger();\n const latestLedger = latestLedgerResp.sequence;\n\n if (cursor === undefined) {\n const cursorRecord = await prisma.indexerCursor.findUnique({\n where: { contractId },\n });\n if (cursorRecord?.lastLedger != null) {\n cursor = cursorRecord.lastLedger;\n } else if (environment.stellar.indexerStartLedger != null) {\n cursor = environment.stellar.indexerStartLedger;\n } else {\n cursor = latestLedger;\n }\n console.log(`Indexer initialized with cursor at ledger ${cursor}`);\n }\n\n const currentCursor = cursor ?? latestLedger;\n cursor = currentCursor;\n\n if (currentCursor > latestLedger) {\n return;\n }\n\n const eventsResp = await sorobanServer.getEvents({\n startLedger: currentCursor,\n filters: [{ type: 'contract', contractIds: [contractId] }],\n limit: 100,\n });\n\n const events = eventsResp.events || [];\n const processedIds: { id: string; topic: string; ledger: number }[] = [];\n\n for (const event of events) {\n try {\n const existing = await prisma.indexerEvent.findUnique({\n where: { id: event.id },\n });\n if (existing) {\n continue;\n }\n\n const topicVal = event.topic && event.topic.length > 0 ? event.topic[0] : undefined;\n const decodedTopic = decodeTopic(topicVal);\n let decodedValue: any = null;\n try {\n decodedValue = event.value ? scValToNative(event.value) : null;\n } catch {\n decodedValue = null;\n }\n\n console.log(`Decoded event [${event.id}] - topic: ${decodedTopic}, value:`, decodedValue);\n\n await dispatch({\n id: event.id,\n topic: decodedTopic,\n ledger: event.ledger,\n txHash: event.txHash,\n data: decodedValue,\n });\n\n processedIds.push({\n id: event.id,\n topic: decodedTopic,\n ledger: event.ledger,\n });\n } catch (err) {\n console.error(`Error processing event ${event.id}:`, err);\n }\n }\n\n const nextCursor =\n events.length === 100 && events[events.length - 1]\n ? events[events.length - 1].ledger + 1\n : latestLedger + 1;\n\n await prisma.$transaction(async tx => {\n for (const item of processedIds) {\n await tx.indexerEvent.create({\n data: {\n id: item.id,\n topic: item.topic,\n ledger: item.ledger,\n },\n });\n }\n await tx.indexerCursor.upsert({\n where: { contractId },\n update: { lastLedger: nextCursor },\n create: { contractId, lastLedger: nextCursor },\n });\n });\n\n cursor = nextCursor;\n } catch (error) {\n console.error('Error in poller tick:', error);\n if (!environment.stellar.contractId || environment.stellar.contractId.trim() === '') {\n throw error;\n }\n }\n}\n\nexport async function startPolling(intervalMs = 6000): Promise {\n const contractId = environment.stellar.contractId;\n if (!contractId || contractId.trim() === '') {\n throw new Error('STELLAR_CONTRACT_ID environment variable is unset or empty');\n }\n if (isRunning) return;\n isRunning = true;\n console.log(`Starting Soroban indexer poller for contract ${contractId}...`);\n\n while (isRunning) {\n await tick();\n if (!isRunning) break;\n await new Promise(resolve => setTimeout(resolve, intervalMs));\n }\n}\n\nexport function stopPolling(): void {\n isRunning = false;\n}\n\nexport function getCursor(): number | undefined {\n return cursor;\n}\n\nexport function setCursor(val: number | undefined): void {\n cursor = val;\n}\n\nexport function resetPoller(): void {\n stopPolling();\n cursor = undefined;\n}\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\indexer\\registry.ts", - "messages": [ - { - "ruleId": "no-console", - "severity": 1, - "message": "Unexpected console statement. Only these console methods are allowed: warn, error.", - "line": 14, - "column": 5, - "nodeType": "MemberExpression", - "messageId": "limited", - "endLine": 14, - "endColumn": 16, - "suggestions": [ - { - "fix": { "range": [424, 499], "text": "" }, - "messageId": "removeConsole", - "data": { "propertyName": "log" }, - "desc": "Remove the console.log()." - } - ] - } - ], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 1, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import { DecodedEvent } from './types.js';\n\nexport type EventHandler = (event: DecodedEvent) => Promise | void;\n\nconst handlers = new Map();\n\nexport function registerEventHandler(topic: string, handler: EventHandler): void {\n handlers.set(topic, handler);\n}\n\nexport async function dispatch(event: DecodedEvent): Promise {\n const handler = handlers.get(event.topic);\n if (!handler) {\n console.log(`No handler registered for topic \"${event.topic}\", skipping.`);\n return;\n }\n await handler(event);\n}\n\nexport function clearHandlers(): void {\n handlers.clear();\n}\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\indexer\\run.ts", - "messages": [ - { - "ruleId": "no-console", - "severity": 1, - "message": "Unexpected console statement. Only these console methods are allowed: warn, error.", - "line": 4, - "column": 3, - "nodeType": "MemberExpression", - "messageId": "limited", - "endLine": 4, - "endColumn": 14, - "suggestions": [ - { - "fix": { "range": [89, 146], "text": "" }, - "messageId": "removeConsole", - "data": { "propertyName": "log" }, - "desc": "Remove the console.log()." - } - ] - }, - { - "ruleId": "no-console", - "severity": 1, - "message": "Unexpected console statement. Only these console methods are allowed: warn, error.", - "line": 9, - "column": 3, - "nodeType": "MemberExpression", - "messageId": "limited", - "endLine": 9, - "endColumn": 14, - "suggestions": [ - { - "fix": { "range": [201, 259], "text": "" }, - "messageId": "removeConsole", - "data": { "propertyName": "log" }, - "desc": "Remove the console.log()." - } - ] - } - ], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 2, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import { startPolling, stopPolling } from './poller.js';\n\nprocess.on('SIGINT', () => {\n console.log('Received SIGINT, shutting down indexer...');\n stopPolling();\n});\n\nprocess.on('SIGTERM', () => {\n console.log('Received SIGTERM, shutting down indexer...');\n stopPolling();\n});\n\nstartPolling().catch(error => {\n console.error('Fatal error starting Soroban indexer:', error);\n process.exit(1);\n});\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\indexer\\sorobanClient.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\indexer\\types.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/no-explicit-any", - "severity": 1, - "message": "Unexpected any. Specify a different type.", - "line": 6, - "column": 9, - "nodeType": "TSAnyKeyword", - "messageId": "unexpectedAny", - "endLine": 6, - "endColumn": 12, - "suggestions": [ - { - "messageId": "suggestUnknown", - "fix": { "range": [107, 110], "text": "unknown" }, - "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct." - }, - { - "messageId": "suggestNever", - "fix": { "range": [107, 110], "text": "never" }, - "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of." - } - ] - } - ], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 1, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "export interface DecodedEvent {\n id: string;\n topic: string;\n ledger: number;\n txHash: string;\n data: any;\n}\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\middlewares\\auth.middleware.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 19, - "column": 56, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 19, - "endColumn": 58 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 32, - "column": 47, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 32, - "endColumn": 49 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 45, - "column": 56, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 45, - "endColumn": 58 - } - ], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 3, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import { Request, Response, NextFunction } from 'express';\nimport jwt from 'jsonwebtoken';\nimport prisma from '../config/prisma.js';\nimport { environment } from '../config/environment.js';\nimport { authenticateApiKey } from '../services/api-key.services.js';\nimport { isApiKeyToken } from '../utils/api-key.utils.js';\n\nconst extractBearerToken = (req: Request): string | null => {\n const authHeader = req.headers.authorization;\n\n if (!authHeader || !authHeader.startsWith('Bearer ')) {\n return null;\n }\n\n const token = authHeader.slice('Bearer '.length).trim();\n return token || null;\n};\n\nconst authenticateRefreshToken = async (token: string) => {\n const session = await prisma.refreshToken.findUnique({\n where: { token },\n include: { merchant: true },\n });\n\n if (!session || session.expiresAt.getTime() < Date.now()) {\n return null;\n }\n\n return session.merchant;\n};\n\nconst authenticateJwt = async (token: string) => {\n try {\n const payload = jwt.verify(token, environment.jwtSecret) as { sub?: string };\n if (!payload.sub) {\n return null;\n }\n\n return prisma.merchant.findUnique({ where: { id: payload.sub } });\n } catch {\n return null;\n }\n};\n\nconst resolveMerchantFromToken = async (token: string) => {\n if (isApiKeyToken(token)) {\n return authenticateApiKey(token);\n }\n\n if (token.split('.').length === 3) {\n return authenticateJwt(token);\n }\n\n return authenticateRefreshToken(token);\n};\n\n/**\n * Authenticates API key bearer tokens, updates lastUsedAt, and attaches the merchant.\n */\nexport const apiKeyAuth = async (\n req: Request,\n res: Response,\n next: NextFunction,\n): Promise => {\n try {\n const token = extractBearerToken(req);\n\n if (!token) {\n res.status(401).json({ error: 'Authentication required' });\n return;\n }\n\n if (!isApiKeyToken(token)) {\n res.status(401).json({ error: 'Invalid or expired token' });\n return;\n }\n\n const merchant = await authenticateApiKey(token);\n if (!merchant) {\n res.status(401).json({ error: 'Invalid or expired token' });\n return;\n }\n\n req.merchant = merchant;\n next();\n } catch {\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n\n/**\n * Authenticates a merchant using refresh tokens or JWT access tokens only.\n * API keys are rejected to prevent key-management operations via API keys.\n */\nexport const authenticateSessionOnly = async (\n req: Request,\n res: Response,\n next: NextFunction,\n): Promise => {\n try {\n const token = extractBearerToken(req);\n\n if (!token) {\n res.status(401).json({ error: 'Authentication required' });\n return;\n }\n\n if (isApiKeyToken(token)) {\n res.status(401).json({ error: 'Invalid or expired token' });\n return;\n }\n\n const merchant =\n token.split('.').length === 3\n ? await authenticateJwt(token)\n : await authenticateRefreshToken(token);\n\n if (!merchant) {\n res.status(401).json({ error: 'Invalid or expired token' });\n return;\n }\n\n req.merchant = merchant;\n next();\n } catch {\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n\n/**\n * Authenticates a merchant from a bearer token.\n *\n * Accepts JWT access tokens (signed with `JWT_SECRET`), refresh session tokens,\n * or API keys. The resolved Merchant is attached to `req.merchant` on success.\n *\n * Responds with 401 when the `Authorization: Bearer ` header is missing\n * or malformed (`Authentication required`), or when the token is invalid,\n * expired, or references a merchant that no longer exists\n * (`Invalid or expired token`).\n */\nexport const authenticateMerchant = async (\n req: Request,\n res: Response,\n next: NextFunction,\n): Promise => {\n try {\n const token = extractBearerToken(req);\n\n if (!token) {\n res.status(401).json({ error: 'Authentication required' });\n return;\n }\n\n const merchant = await resolveMerchantFromToken(token);\n if (!merchant) {\n res.status(401).json({ error: 'Invalid or expired token' });\n return;\n }\n\n req.merchant = merchant;\n next();\n } catch {\n res.status(500).json({ error: 'Internal Server Error' });\n }\n};\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\routes\\auth.routes.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\routes\\index.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\routes\\invoice.routes.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\routes\\merchant.routes.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\routes\\pay.routes.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\server.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 4, - "column": 30, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 4, - "endColumn": 32 - }, - { - "ruleId": "no-console", - "severity": 1, - "message": "Unexpected console statement. Only these console methods are allowed: warn, error.", - "line": 8, - "column": 7, - "nodeType": "MemberExpression", - "messageId": "limited", - "endLine": 8, - "endColumn": 18, - "suggestions": [ - { - "fix": { "range": [201, 290], "text": "" }, - "messageId": "removeConsole", - "data": { "propertyName": "log" }, - "desc": "Remove the console.log()." - } - ] - } - ], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 2, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import app from './app.js';\nimport { environment } from './config/environment.js';\n\nconst startServer = async () => {\n try {\n // Start Express server\n app.listen(environment.port, () => {\n console.log(`Server running on port ${environment.port} in ${environment.nodeEnv} mode`);\n });\n } catch (error) {\n console.error('Error starting server:', error);\n process.exit(1);\n }\n};\n\nstartServer();\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\services\\api-key.services.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 34, - "column": 48, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 34, - "endColumn": 50 - } - ], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 1, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import { Merchant } from '@prisma/client';\nimport prisma from '../config/prisma.js';\nimport { AppError } from '../utils/errors.js';\nimport { generateApiKeyMaterial, hashApiKey, MAX_ACTIVE_API_KEYS } from '../utils/api-key.utils.js';\n\nexport type ApiKeySummary = {\n id: string;\n prefix: string;\n label: string | null;\n lastUsedAt: Date | null;\n createdAt: Date;\n};\n\nexport type CreateApiKeyResult = ApiKeySummary & {\n key: string;\n};\n\ntype ApiKeyListRow = {\n id: string;\n prefix: string | null;\n name: string | null;\n lastUsedAt: Date | null;\n createdAt: Date;\n};\n\nconst toApiKeySummary = (apiKey: ApiKeyListRow): ApiKeySummary => ({\n id: apiKey.id,\n prefix: apiKey.prefix ?? '',\n label: apiKey.name,\n lastUsedAt: apiKey.lastUsedAt,\n createdAt: apiKey.createdAt,\n});\n\nconst activeApiKeyWhere = (merchantId: string) => ({\n merchantId,\n revokedAt: null,\n OR: [{ expiresAt: null }, { expiresAt: { gt: new Date() } }],\n});\n\nexport const createApiKey = async (\n merchantId: string,\n label?: string,\n): Promise => {\n const { rawKey, prefix, keyHash } = generateApiKeyMaterial();\n const normalizedLabel = label?.trim() || null;\n\n const apiKey = await prisma.$transaction(async tx => {\n const activeKeys = await tx.apiKey.count({\n where: activeApiKeyWhere(merchantId),\n });\n\n if (activeKeys >= MAX_ACTIVE_API_KEYS) {\n throw new AppError(400, `Maximum of ${MAX_ACTIVE_API_KEYS} active API keys allowed`);\n }\n\n return tx.apiKey.create({\n data: {\n merchantId,\n keyHash,\n prefix,\n name: normalizedLabel,\n },\n });\n });\n\n return {\n ...toApiKeySummary(apiKey),\n key: rawKey,\n };\n};\n\nexport const listApiKeys = async (merchantId: string): Promise => {\n const apiKeys = await prisma.apiKey.findMany({\n where: {\n merchantId,\n revokedAt: null,\n },\n orderBy: { createdAt: 'desc' },\n select: {\n id: true,\n prefix: true,\n name: true,\n lastUsedAt: true,\n createdAt: true,\n },\n });\n\n return apiKeys.map(toApiKeySummary);\n};\n\nexport const revokeApiKey = async (merchantId: string, keyId: string): Promise => {\n const apiKey = await prisma.apiKey.findFirst({\n where: { id: keyId, merchantId },\n });\n\n if (!apiKey) {\n throw new AppError(404, 'API key not found');\n }\n\n if (apiKey.revokedAt) {\n throw new AppError(400, 'API key already revoked');\n }\n\n await prisma.apiKey.update({\n where: { id: keyId },\n data: { revokedAt: new Date() },\n });\n};\n\nexport const authenticateApiKey = async (rawKey: string): Promise => {\n const keyHash = hashApiKey(rawKey);\n const apiKey = await prisma.apiKey.findUnique({\n where: { keyHash },\n include: { merchant: true },\n });\n\n if (!apiKey || apiKey.revokedAt) {\n return null;\n }\n\n if (apiKey.expiresAt && apiKey.expiresAt.getTime() < Date.now()) {\n return null;\n }\n\n await prisma.apiKey.update({\n where: { id: apiKey.id },\n data: { lastUsedAt: new Date() },\n });\n\n return apiKey.merchant;\n};\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\services\\auth.services.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 19, - "column": 8, - "nodeType": "FunctionDeclaration", - "messageId": "missingReturnType", - "endLine": 19, - "endColumn": 34 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 19, - "column": 8, - "nodeType": "FunctionDeclaration", - "messageId": "missingReturnType", - "endLine": 19, - "endColumn": 34 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 32, - "column": 8, - "nodeType": "FunctionDeclaration", - "messageId": "missingReturnType", - "endLine": 32, - "endColumn": 38 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 32, - "column": 8, - "nodeType": "FunctionDeclaration", - "messageId": "missingReturnType", - "endLine": 32, - "endColumn": 38 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 71, - "column": 8, - "nodeType": "FunctionDeclaration", - "messageId": "missingReturnType", - "endLine": 71, - "endColumn": 37 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 71, - "column": 8, - "nodeType": "FunctionDeclaration", - "messageId": "missingReturnType", - "endLine": 71, - "endColumn": 37 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 98, - "column": 8, - "nodeType": "FunctionDeclaration", - "messageId": "missingReturnType", - "endLine": 98, - "endColumn": 41 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 98, - "column": 8, - "nodeType": "FunctionDeclaration", - "messageId": "missingReturnType", - "endLine": 98, - "endColumn": 41 - } - ], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 8, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import crypto from 'node:crypto';\nimport jwt from 'jsonwebtoken';\nimport { Keypair } from '@stellar/stellar-sdk';\nimport prisma from '../config/prisma.js';\nimport { environment } from '../config/environment.js';\n\nconst NONCE_EXPIRY_MS = 5 * 60 * 1000;\nconst REFRESH_TOKEN_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000;\n\nexport function buildChallengeMessage(address: string, nonce: string, createdAt: Date): string {\n return [\n 'Shade Authentication',\n `Address: ${address}`,\n `Nonce: ${nonce}`,\n `Timestamp: ${createdAt.toISOString()}`,\n ].join('\\n');\n}\n\nexport async function createNonce(address: string) {\n const nonce = crypto.randomUUID();\n const createdAt = new Date();\n const expiresAt = new Date(createdAt.getTime() + NONCE_EXPIRY_MS);\n const message = buildChallengeMessage(address, nonce, createdAt);\n\n const authNonce = await prisma.authNonce.create({\n data: { address, nonce, message, expiresAt },\n });\n\n return { nonce: authNonce.nonce, message: authNonce.message, expiresAt: authNonce.expiresAt };\n}\n\nexport async function verifySignature(address: string, nonce: string, rawSignature: string) {\n const authNonce = await prisma.authNonce.findUnique({ where: { nonce } });\n if (!authNonce) {\n return { valid: false, reason: 'Nonce not found' } as const;\n }\n if (authNonce.address !== address) {\n return { valid: false, reason: 'Address mismatch' } as const;\n }\n if (authNonce.usedAt) {\n return { valid: false, reason: 'Nonce already used' } as const;\n }\n if (new Date() > authNonce.expiresAt) {\n return { valid: false, reason: 'Nonce expired' } as const;\n }\n\n const message = buildChallengeMessage(address, authNonce.nonce, authNonce.createdAt);\n const messageBytes = Buffer.from(message, 'utf-8');\n const signatureBytes = Buffer.from(rawSignature, 'hex');\n\n let isValid: boolean;\n try {\n const keypair = Keypair.fromPublicKey(address);\n isValid = keypair.verify(messageBytes, signatureBytes);\n } catch {\n return { valid: false, reason: 'Invalid address or signature format' } as const;\n }\n\n if (!isValid) {\n return { valid: false, reason: 'Signature verification failed' } as const;\n }\n\n await prisma.authNonce.update({\n where: { id: authNonce.id },\n data: { usedAt: new Date() },\n });\n\n return { valid: true, reason: null } as const;\n}\n\nexport async function upsertMerchant(address: string) {\n const existing = await prisma.merchant.findFirst({ where: { address } });\n if (existing) {\n return existing;\n }\n const merchantId = crypto.randomInt(100_000, 999_999);\n const merchant = await prisma.merchant.create({\n data: { merchantId, address },\n });\n return merchant;\n}\n\nexport function issueAccessToken(merchantId: string, address: string): string {\n return jwt.sign({ sub: merchantId, address }, environment.jwtSecret, { expiresIn: '15m' });\n}\n\nexport async function issueRefreshToken(merchantId: string): Promise {\n const token = crypto.randomUUID();\n const expiresAt = new Date(Date.now() + REFRESH_TOKEN_EXPIRY_MS);\n\n await prisma.refreshToken.create({\n data: { merchantId, token, expiresAt },\n });\n\n return token;\n}\n\nexport async function authenticateWallet(address: string, nonce: string, signature: string) {\n const verification = await verifySignature(address, nonce, signature);\n if (!verification.valid) {\n return { success: false, reason: verification.reason } as const;\n }\n\n const merchant = await upsertMerchant(address);\n const accessToken = issueAccessToken(merchant.id, merchant.address);\n const refreshToken = await issueRefreshToken(merchant.id);\n\n return {\n success: true,\n accessToken,\n refreshToken,\n merchant: {\n id: merchant.id,\n address: merchant.address,\n isRegistered: merchant.registered,\n },\n } as const;\n}\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\services\\email.service.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 20, - "column": 64, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 20, - "endColumn": 66 - }, - { - "ruleId": "no-console", - "severity": 1, - "message": "Unexpected console statement. Only these console methods are allowed: warn, error.", - "line": 96, - "column": 7, - "nodeType": "MemberExpression", - "messageId": "limited", - "endLine": 96, - "endColumn": 18, - "suggestions": [ - { - "fix": { "range": [2612, 2690], "text": "" }, - "messageId": "removeConsole", - "data": { "propertyName": "log" }, - "desc": "Remove the console.log()." - } - ] - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 100, - "column": 73, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 100, - "endColumn": 75 - }, - { - "ruleId": "no-console", - "severity": 1, - "message": "Unexpected console statement. Only these console methods are allowed: warn, error.", - "line": 142, - "column": 7, - "nodeType": "MemberExpression", - "messageId": "limited", - "endLine": 142, - "endColumn": 18, - "suggestions": [ - { - "fix": { "range": [4583, 4675], "text": "" }, - "messageId": "removeConsole", - "data": { "propertyName": "log" }, - "desc": "Remove the console.log()." - } - ] - } - ], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 4, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import nodemailer from 'nodemailer';\nimport { Resend } from 'resend';\nimport { environment } from '../config/environment.js';\nimport type { Invoice, Merchant } from '@prisma/client';\nimport { generateInvoicePdf } from './invoice-pdf.services.js';\n\nexport interface EmailAttachment {\n filename: string;\n content: Buffer;\n}\n\nconst escapeHtml = (value: string): string =>\n value\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n\nconst buildOtpEmailContent = (firstName: string, code: string) => {\n const safeFirstName = escapeHtml(firstName);\n const subject = 'Verify your Shade email';\n const html = `\n

Hi ${safeFirstName},

\n

Your email verification code is:

\n

${code}

\n

This code expires in 10 minutes.

\n `.trim();\n const text = `Hi ${firstName},\\n\\nYour verification code is: ${code}\\n\\nThis code expires in 10 minutes.`;\n\n return { subject, html, text };\n};\n\nconst sendViaResend = async (\n to: string,\n subject: string,\n html: string,\n attachments?: EmailAttachment[],\n): Promise => {\n const resend = new Resend(environment.email.resendApiKey);\n const { error } = await resend.emails.send({\n from: environment.email.from,\n to,\n subject,\n html,\n attachments: attachments?.map(({ filename, content }) => ({ filename, content })),\n });\n\n if (error) {\n throw new Error(`Failed to send email via Resend: ${error.message}`);\n }\n};\n\nconst sendViaSmtp = async (\n to: string,\n subject: string,\n html: string,\n text: string,\n attachments?: EmailAttachment[],\n): Promise => {\n const transporter = nodemailer.createTransport({\n host: environment.email.smtp.host,\n port: environment.email.smtp.port,\n secure: environment.email.smtp.secure,\n auth: {\n user: environment.email.smtp.user,\n pass: environment.email.smtp.pass,\n },\n });\n\n await transporter.sendMail({\n from: environment.email.from,\n to,\n subject,\n html,\n text,\n attachments,\n });\n};\n\n/**\n * Delivers a one-time verification code to the merchant's email address.\n */\nexport const sendOtp = async (to: string, code: string, firstName: string): Promise => {\n const { subject, html, text } = buildOtpEmailContent(firstName, code);\n\n switch (environment.email.provider) {\n case 'resend':\n await sendViaResend(to, subject, html);\n return;\n case 'smtp':\n await sendViaSmtp(to, subject, html, text);\n return;\n case 'console':\n default:\n console.log(`[OTP] Verification code ${code} sent to ${to} for ${firstName}`);\n }\n};\n\nconst buildInvoiceEmailContent = (invoice: Invoice, merchant: Merchant) => {\n const merchantName = escapeHtml(merchant.businessName || 'Your merchant');\n const description = escapeHtml(invoice.description);\n const subject = `Invoice from ${merchant.businessName || 'Shade'}: ${invoice.description}`;\n const html = `\n

Hi,

\n

${merchantName} has sent you an invoice for ${description}.

\n

Amount: ${invoice.amount.toString()} ${escapeHtml(invoice.token)}

\n

Status: ${invoice.status}

\n

Your invoice is attached as a PDF.

\n `.trim();\n const text = `Hi,\\n\\n${merchant.businessName || 'Your merchant'} has sent you an invoice for ${invoice.description}.\\n\\nAmount: ${invoice.amount.toString()} ${invoice.token}\\nStatus: ${invoice.status}\\n\\nYour invoice is attached as a PDF.`;\n\n return { subject, html, text };\n};\n\n/**\n * Emails the invoice to `invoice.email` with a freshly generated PDF attached.\n * No-ops (does not throw) when the invoice has no email on file — callers\n * that need to surface that as a user-facing error (e.g. the /send route)\n * should check `invoice.email` before calling this.\n */\nexport const sendInvoiceEmail = async (invoice: Invoice, merchant: Merchant): Promise => {\n if (!invoice.email) {\n return;\n }\n\n const pdf = await generateInvoicePdf(invoice, merchant);\n const { subject, html, text } = buildInvoiceEmailContent(invoice, merchant);\n const attachments: EmailAttachment[] = [\n { filename: `invoice-${invoice.paymentSlug}.pdf`, content: pdf },\n ];\n\n switch (environment.email.provider) {\n case 'resend':\n await sendViaResend(invoice.email, subject, html, attachments);\n return;\n case 'smtp':\n await sendViaSmtp(invoice.email, subject, html, text, attachments);\n return;\n case 'console':\n default:\n console.log(`[Invoice email] Invoice ${invoice.paymentSlug} (${pdf.length} byte PDF) sent`);\n }\n};\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\services\\index.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\services\\invoice-pdf.services.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\services\\invoice.services.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 28, - "column": 51, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 28, - "endColumn": 53 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 28, - "column": 51, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 28, - "endColumn": 53 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 49, - "column": 83, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 49, - "endColumn": 85 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 49, - "column": 83, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 49, - "endColumn": 85 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 88, - "column": 3, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 88, - "endColumn": 5 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 88, - "column": 3, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 88, - "endColumn": 5 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 125, - "column": 66, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 125, - "endColumn": 68 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 125, - "column": 66, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 125, - "endColumn": 68 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 142, - "column": 78, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 142, - "endColumn": 80 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 142, - "column": 78, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 142, - "endColumn": 80 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 155, - "column": 67, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 155, - "endColumn": 69 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 155, - "column": 67, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 155, - "endColumn": 69 - } - ], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 12, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import type { Invoice, InvoiceStatus as PrismaInvoiceStatus, Prisma } from '@prisma/client';\nimport prisma from '../config/prisma.js';\nimport { AppError } from '../utils/errors.js';\nimport { generatePaymentSlug } from '../utils/slug.js';\nimport {\n CreateInvoiceInput,\n InvoiceListFilters,\n InvoicePagination,\n parseAmount,\n} from '../utils/invoice.validation.js';\n\nconst SLUG_MAX_RETRIES = 5;\n\n// String constants matching the Prisma `Status` enum. Defined locally so this\n// module never imports a runtime value from `@prisma/client` (the generated\n// client is mocked in tests and not generated in CI).\nconst InvoiceStatus = {\n DRAFT: 'DRAFT',\n PENDING: 'PENDING',\n PAID: 'PAID',\n CANCELLED: 'CANCELLED',\n} as const satisfies Record;\n\n/**\n * Public-facing view of an invoice. `amount` is serialized to a string because\n * `BigInt` is not JSON-serializable.\n */\nexport const sanitizeInvoice = (invoice: Invoice) => ({\n id: invoice.id,\n paymentSlug: invoice.paymentSlug,\n description: invoice.description,\n amount: invoice.amount.toString(),\n token: invoice.token,\n status: invoice.status,\n merchantId: invoice.merchantId,\n email: invoice.email,\n expiresAt: invoice.expiresAt,\n datePaid: invoice.datePaid,\n createdAt: invoice.createdAt,\n updatedAt: invoice.updatedAt,\n});\n\nconst isUniqueSlugError = (error: unknown): boolean => {\n if (typeof error !== 'object' || error === null) return false;\n const { code, meta } = error as { code?: string; meta?: { target?: unknown } };\n return code === 'P2002' && Array.isArray(meta?.target) && meta.target.includes('paymentSlug');\n};\n\nexport const createInvoice = async (merchantId: string, data: CreateInvoiceInput) => {\n const amount = parseAmount(data.amount);\n if (amount === null) {\n throw new AppError(400, 'amount must be a positive integer');\n }\n\n const status: PrismaInvoiceStatus = data.isDraft ? InvoiceStatus.DRAFT : InvoiceStatus.PENDING;\n const expiresAt = data.expiresAt ? new Date(data.expiresAt) : null;\n\n for (let attempt = 0; attempt < SLUG_MAX_RETRIES; attempt++) {\n try {\n const invoice = await prisma.invoice.create({\n data: {\n merchantId,\n description: data.description.trim(),\n amount,\n token: data.token.trim(),\n email: data.payerEmail?.trim() ?? null,\n expiresAt,\n status,\n paymentSlug: generatePaymentSlug(),\n },\n });\n return sanitizeInvoice(invoice);\n } catch (error) {\n if (isUniqueSlugError(error) && attempt < SLUG_MAX_RETRIES - 1) {\n continue;\n }\n throw error;\n }\n }\n\n throw new AppError(500, 'Failed to generate a unique payment slug');\n};\n\nexport const listInvoices = async (\n merchantId: string,\n filters: InvoiceListFilters,\n pagination: InvoicePagination,\n) => {\n const where: Prisma.InvoiceWhereInput = { merchantId };\n\n if (filters.status) {\n where.status = filters.status;\n }\n\n if (filters.token) {\n where.token = filters.token;\n }\n\n if (filters.startDate || filters.endDate) {\n where.createdAt = {};\n if (filters.startDate) where.createdAt.gte = filters.startDate;\n if (filters.endDate) where.createdAt.lte = filters.endDate;\n }\n\n const [invoices, total] = await Promise.all([\n prisma.invoice.findMany({\n where,\n take: pagination.limit,\n skip: pagination.offset,\n orderBy: { createdAt: 'desc' },\n }),\n prisma.invoice.count({ where }),\n ]);\n\n return {\n data: invoices.map(sanitizeInvoice),\n pagination: {\n limit: pagination.limit,\n offset: pagination.offset,\n total,\n },\n };\n};\n\nexport const getInvoice = async (merchantId: string, id: string) => {\n const invoice = await prisma.invoice.findFirst({\n where: { id, merchantId },\n });\n\n if (!invoice) {\n throw new AppError(404, 'Invoice not found');\n }\n\n return sanitizeInvoice(invoice);\n};\n\n/**\n * Fetches the raw invoice + merchant records, scoped to the owning merchant,\n * for the PDF/email flows that need fields beyond the sanitized public view\n * (payer address, fiat breakdown, merchant logo).\n */\nexport const getInvoiceWithMerchant = async (merchantId: string, id: string) => {\n const invoice = await prisma.invoice.findFirst({\n where: { id, merchantId },\n include: { merchant: true },\n });\n\n if (!invoice) {\n throw new AppError(404, 'Invoice not found');\n }\n\n return invoice;\n};\n\nexport const voidInvoice = async (merchantId: string, id: string) => {\n const invoice = await prisma.invoice.findFirst({\n where: { id, merchantId },\n });\n\n if (!invoice) {\n throw new AppError(404, 'Invoice not found');\n }\n\n if (invoice.status !== InvoiceStatus.PENDING) {\n throw new AppError(400, 'Only pending invoices can be voided');\n }\n\n const updated = await prisma.invoice.update({\n where: { id: invoice.id },\n data: { status: InvoiceStatus.CANCELLED },\n });\n\n return sanitizeInvoice(updated);\n};\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\services\\merchant.services.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 23, - "column": 54, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 23, - "endColumn": 56 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 23, - "column": 54, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 23, - "endColumn": 56 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 45, - "column": 66, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 45, - "endColumn": 68 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 45, - "column": 66, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 45, - "endColumn": 68 - }, - { - "ruleId": "no-useless-catch", - "severity": 2, - "message": "Unnecessary try/catch wrapper.", - "line": 46, - "column": 3, - "nodeType": "TryStatement", - "messageId": "unnecessaryCatch", - "endLine": 53, - "endColumn": 4 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 56, - "column": 55, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 56, - "endColumn": 57 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 56, - "column": 55, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 56, - "endColumn": 57 - }, - { - "ruleId": "no-useless-catch", - "severity": 2, - "message": "Unnecessary try/catch wrapper.", - "line": 57, - "column": 3, - "nodeType": "TryStatement", - "messageId": "unnecessaryCatch", - "endLine": 66, - "endColumn": 4 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 69, - "column": 68, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 69, - "endColumn": 70 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 69, - "column": 68, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 69, - "endColumn": 70 - }, - { - "ruleId": "no-useless-catch", - "severity": 2, - "message": "Unnecessary try/catch wrapper.", - "line": 70, - "column": 3, - "nodeType": "TryStatement", - "messageId": "unnecessaryCatch", - "endLine": 78, - "endColumn": 4 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 88, - "column": 89, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 88, - "endColumn": 91 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 88, - "column": 89, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 88, - "endColumn": 91 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 147, - "column": 48, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 147, - "endColumn": 50 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 147, - "column": 48, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 147, - "endColumn": 50 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 168, - "column": 62, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 168, - "endColumn": 64 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 168, - "column": 62, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 168, - "endColumn": 64 - }, - { - "ruleId": "no-console", - "severity": 1, - "message": "Unexpected console statement. Only these console methods are allowed: warn, error.", - "line": 192, - "column": 3, - "nodeType": "MemberExpression", - "messageId": "limited", - "endLine": 192, - "endColumn": 15, - "suggestions": [ - { - "fix": { "range": [5615, 5730], "text": "" }, - "messageId": "removeConsole", - "data": { "propertyName": "info" }, - "desc": "Remove the console.info()." - } - ] - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 206, - "column": 78, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 206, - "endColumn": 80 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 206, - "column": 78, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 206, - "endColumn": 80 - } - ], - "suppressedMessages": [], - "errorCount": 3, - "fatalErrorCount": 0, - "warningCount": 17, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import { Merchant, Prisma } from '@prisma/client';\nimport prisma from '../config/prisma.js';\nimport { AppError } from '../utils/errors.js';\nimport { RegisterMerchantInput, UpdateMerchantInput } from '../utils/validation.js';\nimport { generateOtp, hashOtp } from './otp.services.js';\nimport { sendOtp } from './email.service.js';\nimport { Keypair } from '@stellar/stellar-sdk';\n\nconst OTP_EXPIRY_MS = 10 * 60 * 1000;\n\ninterface MerchantData {\n merchantId: number;\n email?: string;\n address: string;\n active?: boolean;\n verified?: boolean;\n}\n\n/**\n * Returns a public-facing view of a merchant. Built as an allow-list so that\n * any sensitive fields added to the model later are never exposed by default.\n */\nexport const sanitizeMerchant = (merchant: Merchant) => ({\n id: merchant.id,\n merchantId: merchant.merchantId,\n email: merchant.email,\n address: merchant.address,\n account: merchant.account,\n merchantKey: merchant.merchantKey,\n firstName: merchant.firstName,\n lastName: merchant.lastName,\n businessName: merchant.businessName,\n category: merchant.category,\n description: merchant.description,\n logo: merchant.logo,\n webhook: merchant.webhook,\n active: merchant.active,\n verified: merchant.verified,\n emailVerified: merchant.emailVerified,\n registered: merchant.registered,\n createdAt: merchant.createdAt,\n updatedAt: merchant.updatedAt,\n});\n\nexport const createMerchant = async (merchantData: MerchantData) => {\n try {\n const merchant = await prisma.merchant.create({\n data: merchantData,\n });\n return merchant;\n } catch (error) {\n throw error;\n }\n};\n\nexport const getMerchant = async (merchantId: number) => {\n try {\n const merchant = await prisma.merchant.findUnique({\n where: {\n merchantId: merchantId,\n },\n });\n return merchant;\n } catch (error) {\n throw error;\n }\n};\n\nexport const listMerchants = async (limit: number, offset: number) => {\n try {\n const merchants = await prisma.merchant.findMany({\n take: limit,\n skip: offset,\n });\n return merchants;\n } catch (error) {\n throw error;\n }\n};\n\n/**\n * Completes a merchant's profile after wallet authentication.\n *\n * Enforces that the email is unique across merchants and that the profile has\n * not already been completed, persists the profile data, resets email\n * verification, and triggers an OTP email.\n */\nexport const registerMerchant = async (merchantId: string, data: RegisterMerchantInput) => {\n const merchant = await prisma.merchant.findUnique({\n where: { id: merchantId },\n });\n\n if (!merchant) {\n throw new AppError(404, 'Merchant not found');\n }\n\n if (merchant.registered) {\n throw new AppError(409, 'Profile already set up');\n }\n\n const normalizedEmail = data.email.trim().toLowerCase();\n\n const existingEmail = await prisma.merchant.findFirst({\n where: {\n email: normalizedEmail,\n NOT: { id: merchantId },\n },\n });\n\n if (existingEmail) {\n throw new AppError(409, 'Email already registered');\n }\n\n const code = generateOtp();\n const emailOtp = await hashOtp(code);\n const emailOtpExpiresAt = new Date(Date.now() + OTP_EXPIRY_MS);\n\n const updatedMerchant = await prisma.merchant.update({\n where: { id: merchantId },\n data: {\n firstName: data.firstName.trim(),\n lastName: data.lastName.trim(),\n email: normalizedEmail,\n businessName: data.businessName.trim(),\n category: data.category.trim(),\n description: data.description.trim(),\n logo: data.logo?.trim() ?? null,\n emailVerified: false,\n registered: true,\n emailOtp,\n emailOtpExpiresAt,\n },\n });\n\n try {\n await sendOtp(normalizedEmail, code, data.firstName.trim());\n } catch (err) {\n console.error('Failed to send OTP email after registration', err);\n }\n\n return sanitizeMerchant(updatedMerchant);\n};\n\n/**\n * Returns the authenticated merchant's own profile.\n */\nexport const getMyProfile = async (id: string) => {\n const merchant = await prisma.merchant.findUnique({ where: { id } });\n\n if (!merchant) {\n throw new AppError(404, 'Merchant not found');\n }\n\n return sanitizeMerchant(merchant);\n};\n\n/**\n * Generates a fresh Ed25519 signing keypair for the merchant.\n *\n * Persists ONLY the hex-encoded 32-byte public key to `Merchant.merchantKey`,\n * overwriting any previous value (unconditional generate-and-replace). Returns\n * both halves; the hex-encoded 32-byte private key is returned exactly once and\n * is never written to the database or logged.\n *\n * Uploading the public key on-chain (`set_merchant_key`) and signing invoices\n * with the private key are done client/SDK-side and are out of scope here.\n */\nexport const generateMerchantSigningKey = async (id: string) => {\n const merchant = await prisma.merchant.findUnique({ where: { id } });\n\n if (!merchant) {\n throw new AppError(404, 'Merchant not found');\n }\n\n const keypair = Keypair.random();\n const publicKey = Buffer.from(keypair.rawPublicKey()).toString('hex');\n const privateKey = Buffer.from(keypair.rawSecretKey()).toString('hex');\n\n // Optimistic concurrency: only replace the key we just read. If a concurrent\n // rotation already changed it, no row matches and we reject rather than return\n // a private key whose public half is no longer the one persisted.\n const { count } = await prisma.merchant.updateMany({\n where: { id, merchantKey: merchant.merchantKey },\n data: { merchantKey: publicKey },\n });\n\n if (count !== 1) {\n throw new AppError(409, 'Signing key was changed concurrently; please retry');\n }\n\n // Audit only — never include the private key here.\n console.info(\n `[merchant] signing key ${merchant.merchantKey ? 'rotated' : 'created'} for merchant ${id}`,\n );\n\n return { publicKey, privateKey };\n};\n\n/**\n * Partially updates the authenticated merchant's editable profile fields.\n *\n * Only fields present in `data` are written. Strings are trimmed; an empty\n * `logo`/`webhook` is normalized to null so the merchant can clear them.\n * Non-editable fields are never read here, so they cannot be changed.\n */\nexport const updateMyProfile = async (id: string, data: UpdateMerchantInput) => {\n const updateData: Prisma.MerchantUpdateInput = {};\n\n const textFields = ['firstName', 'lastName', 'businessName', 'category', 'description'] as const;\n for (const field of textFields) {\n const value = data[field];\n if (value !== undefined) {\n updateData[field] = value.trim();\n }\n }\n\n if (data.logo !== undefined) {\n const logo = typeof data.logo === 'string' ? data.logo.trim() : data.logo;\n updateData.logo = logo ? logo : null;\n }\n\n if (data.webhook !== undefined) {\n const webhook = typeof data.webhook === 'string' ? data.webhook.trim() : data.webhook;\n updateData.webhook = webhook ? webhook : null;\n }\n\n const updated = await prisma.merchant.update({ where: { id }, data: updateData });\n\n return sanitizeMerchant(updated);\n};\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\services\\otp.services.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 49, - "column": 72, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 49, - "endColumn": 74 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 49, - "column": 72, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 49, - "endColumn": 74 - } - ], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 2, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import { randomInt } from 'node:crypto';\nimport bcrypt from 'bcrypt';\nimport prisma from '../config/prisma.js';\nimport { AppError } from '../utils/errors.js';\nimport { sendOtp } from './email.service.js';\n\nconst OTP_LENGTH = 6;\nconst OTP_EXPIRY_MS = 10 * 60 * 1000;\nconst OTP_RESEND_COOLDOWN_MS = 60 * 1000;\nconst BCRYPT_ROUNDS = 10;\n\nexport const generateOtp = (): string => {\n const min = 10 ** (OTP_LENGTH - 1);\n const max = 10 ** OTP_LENGTH - 1;\n return randomInt(min, max + 1).toString();\n};\n\nexport const hashOtp = async (code: string): Promise => bcrypt.hash(code, BCRYPT_ROUNDS);\n\nexport const verifyOtpHash = async (code: string, hash: string): Promise =>\n bcrypt.compare(code, hash);\n\nconst getLastOtpSentAt = (expiresAt: Date): Date => new Date(expiresAt.getTime() - OTP_EXPIRY_MS);\n\n/**\n * Generates a 6-digit OTP, stores its bcrypt hash with a 10-minute expiry,\n * and sends the code to the merchant's email.\n */\nexport const issueEmailOtp = async (merchant: {\n id: string;\n email: string;\n firstName: string | null;\n}): Promise => {\n const code = generateOtp();\n const emailOtp = await hashOtp(code);\n const emailOtpExpiresAt = new Date(Date.now() + OTP_EXPIRY_MS);\n\n await prisma.merchant.update({\n where: { id: merchant.id },\n data: { emailOtp, emailOtpExpiresAt },\n });\n\n await sendOtp(merchant.email, code, merchant.firstName?.trim() || 'there');\n};\n\n/**\n * Validates the submitted OTP against the stored hash and marks the email verified.\n */\nexport const verifyEmailOtp = async (merchantId: string, code: string) => {\n const merchant = await prisma.merchant.findUnique({\n where: { id: merchantId },\n });\n\n if (!merchant?.emailOtp || !merchant.emailOtpExpiresAt) {\n throw new AppError(400, 'Invalid verification code');\n }\n\n if (merchant.emailOtpExpiresAt.getTime() < Date.now()) {\n throw new AppError(400, 'Code expired');\n }\n\n const isValid = await verifyOtpHash(code, merchant.emailOtp);\n if (!isValid) {\n throw new AppError(400, 'Invalid verification code');\n }\n\n return prisma.merchant.update({\n where: { id: merchantId },\n data: {\n emailVerified: true,\n emailOtp: null,\n emailOtpExpiresAt: null,\n },\n });\n};\n\n/**\n * Re-generates and re-sends the email OTP, rate-limited to one request per minute.\n */\nexport const resendEmailOtp = async (merchantId: string): Promise => {\n const merchant = await prisma.merchant.findUnique({\n where: { id: merchantId },\n });\n\n if (!merchant) {\n throw new AppError(404, 'Merchant not found');\n }\n\n if (!merchant.registered || !merchant.email) {\n throw new AppError(400, 'Registration incomplete');\n }\n\n if (merchant.emailVerified) {\n throw new AppError(400, 'Email already verified');\n }\n\n if (merchant.emailOtpExpiresAt) {\n const lastSentAt = getLastOtpSentAt(merchant.emailOtpExpiresAt);\n if (Date.now() - lastSentAt.getTime() < OTP_RESEND_COOLDOWN_MS) {\n throw new AppError(429, 'Please wait before requesting a new code');\n }\n }\n\n await issueEmailOtp({\n id: merchant.id,\n email: merchant.email,\n firstName: merchant.firstName,\n });\n};\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\services\\pay.services.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 13, - "column": 97, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 13, - "endColumn": 99 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 27, - "column": 58, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 27, - "endColumn": 60 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 27, - "column": 58, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 27, - "endColumn": 60 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 70, - "column": 60, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 70, - "endColumn": 62 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 70, - "column": 60, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 70, - "endColumn": 62 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 85, - "column": 91, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 85, - "endColumn": 93 - }, - { - "ruleId": "@typescript-eslint/explicit-module-boundary-types", - "severity": 1, - "message": "Missing return type on function.", - "line": 85, - "column": 91, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 85, - "endColumn": 93 - } - ], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 7, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import prisma from '../config/prisma.js';\nimport { AppError } from '../utils/errors.js';\nimport type { InvoiceStatus as PrismaInvoiceStatus } from '@prisma/client';\n\nconst InvoiceStatus = {\n DRAFT: 'DRAFT',\n PENDING: 'PENDING',\n PAID: 'PAID',\n CANCELLED: 'CANCELLED',\n REFUNDED: 'REFUNDED',\n} as const satisfies Record;\n\nconst assertInvoiceVisible = (invoice: { status: PrismaInvoiceStatus; expiresAt: Date | null }) => {\n if (\n invoice.status === InvoiceStatus.CANCELLED ||\n invoice.status === InvoiceStatus.PAID ||\n invoice.status === InvoiceStatus.REFUNDED\n ) {\n throw new AppError(410, 'Invoice is no longer available');\n }\n\n if (invoice.expiresAt && invoice.expiresAt < new Date()) {\n throw new AppError(410, 'expired');\n }\n};\n\nexport const resolveInvoiceBySlug = async (slug: string) => {\n const invoice = await prisma.invoice.findUnique({\n where: { paymentSlug: slug },\n select: {\n paymentSlug: true,\n description: true,\n amount: true,\n token: true,\n status: true,\n expiresAt: true,\n pricingMode: true,\n merchant: {\n select: {\n businessName: true,\n },\n },\n },\n });\n\n if (!invoice) {\n throw new AppError(404, 'Invoice not found');\n }\n\n assertInvoiceVisible(invoice);\n\n return {\n slug: invoice.paymentSlug,\n description: invoice.description,\n amount: invoice.amount.toString(),\n token: invoice.token,\n status: invoice.status,\n merchantName: invoice.merchant.businessName,\n expiresAt: invoice.expiresAt,\n pricingMode: invoice.pricingMode,\n };\n};\n\n/**\n * Fetches the full invoice + merchant records for a publicly visible invoice,\n * applying the same 404/410 visibility rules as `resolveInvoiceBySlug`. Used\n * by the public PDF download route, which needs raw fields (payer, dates,\n * fiat breakdown, logo) rather than the trimmed public-facing view.\n */\nexport const getInvoiceForPdfBySlug = async (slug: string) => {\n const invoice = await prisma.invoice.findUnique({\n where: { paymentSlug: slug },\n include: { merchant: true },\n });\n\n if (!invoice) {\n throw new AppError(404, 'Invoice not found');\n }\n\n assertInvoiceVisible(invoice);\n\n return invoice;\n};\n\nexport const confirmPayment = async (slug: string, payerAddress: string, txHash?: string) => {\n return await prisma.$transaction(async tx => {\n const invoice = await tx.invoice.findUnique({\n where: { paymentSlug: slug },\n });\n\n if (!invoice) {\n throw new AppError(404, 'Invoice not found');\n }\n\n assertInvoiceVisible(invoice);\n\n const idempotencyKey = `${invoice.id}-${payerAddress}-${txHash || 'none'}`;\n\n const confirmation = await tx.paymentConfirmation.upsert({\n where: { idempotencyKey },\n update: {},\n create: {\n invoiceId: invoice.id,\n merchantId: invoice.merchantId,\n payerAddress,\n txHash: txHash || null,\n idempotencyKey,\n },\n });\n\n return confirmation;\n });\n};\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\services\\storage\\invoice-pdf.storage.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\types\\express.d.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\utils\\api-key.utils.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\utils\\errors.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\utils\\invoice.validation.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\utils\\slug.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\src\\utils\\validation.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\__mocks__\\prisma.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/no-unused-vars", - "severity": 2, - "message": "'DeepMockProxy' is defined but never used. Allowed unused vars must match /^_/u.", - "line": 2, - "column": 31, - "nodeType": null, - "messageId": "unusedVar", - "endLine": 2, - "endColumn": 44 - } - ], - "suppressedMessages": [], - "errorCount": 1, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import { jest, beforeEach } from '@jest/globals';\nimport { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended';\nimport { PrismaClient } from '@prisma/client';\n\nexport const prismaMock = mockDeep();\n\njest.mock('../../src/config/prisma.js', () => ({\n __esModule: true,\n default: prismaMock,\n}));\n\nbeforeEach(() => {\n mockReset(prismaMock);\n});\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\helpers\\api-key.fixtures.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\integration\\api-key.routes.test.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/no-unused-vars", - "severity": 2, - "message": "'TEST_API_KEY_PREFIX' is defined but never used. Allowed unused vars must match /^_/u.", - "line": 5, - "column": 3, - "nodeType": null, - "messageId": "unusedVar", - "endLine": 5, - "endColumn": 22 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 20, - "column": 5, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 20, - "endColumn": 20 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 21, - "column": 5, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 21, - "endColumn": 17 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 22, - "column": 5, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 22, - "endColumn": 29 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 68, - "column": 36, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 68, - "endColumn": 38 - } - ], - "suppressedMessages": [], - "errorCount": 1, - "fatalErrorCount": 0, - "warningCount": 4, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import { jest } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\nimport request from 'supertest';\nimport {\n TEST_API_KEY_PREFIX,\n TEST_KEY_HASH,\n TEST_KEY_PREFIX_DISPLAY,\n TEST_RAW_API_KEY,\n} from '../helpers/api-key.fixtures.js';\n\njest.unstable_mockModule('../../src/utils/api-key.utils.js', () => {\n const prefix = 'sk_' + 'live_';\n const rawKey = `${prefix}testkey1234567890123456789012345`;\n return {\n __esModule: true,\n API_KEY_PREFIX: prefix,\n API_KEY_RANDOM_LENGTH: 32,\n API_KEY_DISPLAY_PREFIX_LENGTH: 8,\n MAX_ACTIVE_API_KEYS: 10,\n isApiKeyToken: (token: string) => token.startsWith(prefix),\n hashApiKey: (rawKeyValue: string) => `hash-${rawKeyValue}`,\n generateApiKeyMaterial: () => ({\n rawKey,\n prefix: `${prefix}testkey1`,\n keyHash: `hash-${rawKey}`,\n }),\n };\n});\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { default: app } = await import('../../src/app.js');\n\nconst merchant = {\n id: 'merchant-1',\n merchantId: 1,\n address: '0x123',\n account: null,\n email: 'merchant@example.com',\n firstName: 'Ada',\n lastName: 'Lovelace',\n businessName: 'Engines',\n category: 'software',\n description: 'desc',\n logo: null,\n webhook: null,\n active: true,\n verified: false,\n emailVerified: true,\n registered: true,\n emailOtp: null,\n emailOtpExpiresAt: null,\n createdAt: new Date('2026-06-27T12:00:00.000Z'),\n updatedAt: new Date('2026-06-27T12:00:00.000Z'),\n};\n\nconst baseApiKey = {\n id: 'key-1',\n merchantId: merchant.id,\n keyHash: TEST_KEY_HASH,\n prefix: TEST_KEY_PREFIX_DISPLAY,\n name: 'Production',\n lastUsedAt: null,\n expiresAt: null,\n revokedAt: null,\n createdAt: new Date('2026-06-27T12:00:00.000Z'),\n};\n\nconst authenticateWithSession = () => {\n prismaMock.refreshToken.findUnique.mockResolvedValue({\n id: 'session-1',\n merchantId: merchant.id,\n token: 'valid-token',\n expiresAt: new Date(Date.now() + 60 * 60 * 1000),\n createdAt: new Date(),\n merchant,\n } as any);\n};\n\ndescribe('Merchant API key routes', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n prismaMock.$transaction.mockImplementation(\n async (callback: (tx: typeof prismaMock) => unknown) => callback(prismaMock),\n );\n });\n\n describe('POST /api/v1/merchants/api-keys', () => {\n test('returns 401 when unauthenticated', async () => {\n const response = await request(app)\n .post('/api/v1/merchants/api-keys')\n .send({ label: 'Production' });\n\n expect(response.status).toBe(401);\n });\n\n test('returns 201 with raw key only on creation', async () => {\n authenticateWithSession();\n prismaMock.apiKey.count.mockResolvedValue(0);\n prismaMock.apiKey.create.mockResolvedValue(baseApiKey as any);\n\n const response = await request(app)\n .post('/api/v1/merchants/api-keys')\n .set('Authorization', 'Bearer valid-token')\n .send({ label: 'Production' });\n\n expect(response.status).toBe(201);\n expect(response.body).toEqual({\n id: 'key-1',\n key: TEST_RAW_API_KEY,\n prefix: TEST_KEY_PREFIX_DISPLAY,\n label: 'Production',\n lastUsedAt: null,\n createdAt: baseApiKey.createdAt.toISOString(),\n });\n });\n\n test('returns 400 when label is not a string', async () => {\n authenticateWithSession();\n\n const response = await request(app)\n .post('/api/v1/merchants/api-keys')\n .set('Authorization', 'Bearer valid-token')\n .send({ label: 123 });\n\n expect(response.status).toBe(400);\n expect(response.body).toEqual({ error: 'label must be a string' });\n expect(prismaMock.apiKey.create).not.toHaveBeenCalled();\n });\n\n test('returns 401 when authenticated with an API key', async () => {\n prismaMock.apiKey.findUnique.mockResolvedValue({\n ...baseApiKey,\n merchant,\n } as any);\n\n const response = await request(app)\n .post('/api/v1/merchants/api-keys')\n .set('Authorization', `Bearer ${TEST_RAW_API_KEY}`)\n .send({ label: 'Secondary' });\n\n expect(response.status).toBe(401);\n expect(prismaMock.apiKey.create).not.toHaveBeenCalled();\n });\n\n test('returns 400 when active key limit is exceeded', async () => {\n authenticateWithSession();\n prismaMock.apiKey.count.mockResolvedValue(10);\n\n const response = await request(app)\n .post('/api/v1/merchants/api-keys')\n .set('Authorization', 'Bearer valid-token')\n .send({ label: 'Another key' });\n\n expect(response.status).toBe(400);\n expect(response.body).toEqual({ error: 'Maximum of 10 active API keys allowed' });\n });\n });\n\n describe('GET /api/v1/merchants/api-keys', () => {\n test('returns non-revoked keys without raw key or hash', async () => {\n authenticateWithSession();\n prismaMock.apiKey.findMany.mockResolvedValue([baseApiKey] as any);\n\n const response = await request(app)\n .get('/api/v1/merchants/api-keys')\n .set('Authorization', 'Bearer valid-token');\n\n expect(response.status).toBe(200);\n expect(response.body).toEqual([\n {\n id: 'key-1',\n prefix: TEST_KEY_PREFIX_DISPLAY,\n label: 'Production',\n lastUsedAt: null,\n createdAt: baseApiKey.createdAt.toISOString(),\n },\n ]);\n expect(JSON.stringify(response.body)).not.toContain('keyHash');\n expect(JSON.stringify(response.body)).not.toContain(TEST_RAW_API_KEY);\n });\n });\n\n describe('DELETE /api/v1/merchants/api-keys/:id', () => {\n test('revokes an owned key', async () => {\n authenticateWithSession();\n prismaMock.apiKey.findFirst.mockResolvedValue(baseApiKey as any);\n prismaMock.apiKey.update.mockResolvedValue({ ...baseApiKey, revokedAt: new Date() } as any);\n\n const response = await request(app)\n .delete('/api/v1/merchants/api-keys/key-1')\n .set('Authorization', 'Bearer valid-token');\n\n expect(response.status).toBe(200);\n expect(response.body).toEqual({ message: 'API key revoked' });\n });\n\n test('returns 404 when key belongs to another merchant', async () => {\n authenticateWithSession();\n prismaMock.apiKey.findFirst.mockResolvedValue(null);\n\n const response = await request(app)\n .delete('/api/v1/merchants/api-keys/key-2')\n .set('Authorization', 'Bearer valid-token');\n\n expect(response.status).toBe(404);\n expect(response.body).toEqual({ error: 'API key not found' });\n });\n });\n});\n\ndescribe('API key authentication middleware', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n });\n\n test('allows invoice access with a valid API key and updates lastUsedAt', async () => {\n prismaMock.apiKey.findUnique.mockResolvedValue({\n ...baseApiKey,\n merchant,\n } as any);\n prismaMock.apiKey.update.mockResolvedValue(baseApiKey as any);\n prismaMock.invoice.findMany.mockResolvedValue([]);\n\n const response = await request(app)\n .get('/api/v1/invoices')\n .set('Authorization', `Bearer ${TEST_RAW_API_KEY}`);\n\n expect(response.status).toBe(200);\n expect(prismaMock.apiKey.update).toHaveBeenCalledWith({\n where: { id: 'key-1' },\n data: { lastUsedAt: expect.any(Date) },\n });\n expect(prismaMock.refreshToken.findUnique).not.toHaveBeenCalled();\n });\n\n test('returns 401 for revoked API keys', async () => {\n prismaMock.apiKey.findUnique.mockResolvedValue({\n ...baseApiKey,\n revokedAt: new Date(),\n merchant,\n } as any);\n\n const response = await request(app)\n .get('/api/v1/invoices')\n .set('Authorization', `Bearer ${TEST_RAW_API_KEY}`);\n\n expect(response.status).toBe(401);\n });\n});\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\integration\\auth.email-otp.test.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 45, - "column": 60, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 45, - "endColumn": 62 - } - ], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 1, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import { jest } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\nimport request from 'supertest';\n\nconst sendOtpMock = jest.fn(async () => undefined);\n\njest.unstable_mockModule('../../src/services/email.service.js', () => ({\n __esModule: true,\n sendOtp: sendOtpMock,\n sendInvoiceEmail: jest.fn(async () => undefined),\n}));\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { default: app } = await import('../../src/app.js');\nconst bcrypt = await import('bcrypt');\n\nconst VERIFY_EMAIL_URL = '/api/v1/auth/verify-email';\nconst RESEND_OTP_URL = '/api/v1/auth/resend-otp';\n\nconst mockDate = new Date('2026-06-21T12:00:00Z');\n\nconst registeredMerchant = {\n id: 'uuid-1',\n merchantId: 1,\n address: '0x123',\n account: null,\n email: 'ada@example.com',\n firstName: 'Ada',\n lastName: 'Lovelace',\n businessName: 'Analytical Engines',\n category: 'software',\n description: 'We build computing machines.',\n logo: null,\n webhook: null,\n active: true,\n verified: false,\n emailVerified: false,\n registered: true,\n emailOtp: null as string | null,\n emailOtpExpiresAt: null as Date | null,\n createdAt: mockDate,\n updatedAt: mockDate,\n};\n\nconst authenticateAs = (merchant: Record) => {\n prismaMock.refreshToken.findUnique.mockResolvedValue({\n id: 'session-1',\n merchantId: merchant.id,\n token: 'valid-token',\n expiresAt: new Date(Date.now() + 60 * 60 * 1000),\n createdAt: mockDate,\n merchant,\n } as any);\n};\n\ndescribe('Email OTP auth routes', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n sendOtpMock.mockClear();\n jest.useFakeTimers({ now: mockDate });\n });\n\n afterEach(() => {\n jest.useRealTimers();\n });\n\n describe('POST /api/v1/auth/verify-email', () => {\n test('returns 401 for unauthenticated requests', async () => {\n const response = await request(app).post(VERIFY_EMAIL_URL).send({ code: '123456' });\n\n expect(response.status).toBe(401);\n expect(response.body).toEqual({ error: 'Authentication required' });\n });\n\n test('returns 400 when code is missing', async () => {\n authenticateAs(registeredMerchant);\n\n const response = await request(app)\n .post(VERIFY_EMAIL_URL)\n .set('Authorization', 'Bearer valid-token')\n .send({});\n\n expect(response.status).toBe(400);\n expect(response.body).toEqual({ error: 'code is required' });\n });\n\n test('returns 200 and marks emailVerified true with correct code', async () => {\n const code = '123456';\n const emailOtp = await bcrypt.hash(code, 10);\n const merchantWithOtp = {\n ...registeredMerchant,\n emailOtp,\n emailOtpExpiresAt: new Date('2026-06-21T12:05:00.000Z'),\n };\n\n authenticateAs(merchantWithOtp);\n prismaMock.merchant.findUnique.mockResolvedValue(merchantWithOtp as any);\n prismaMock.merchant.update.mockImplementation(async (args: any) => ({\n ...merchantWithOtp,\n ...args.data,\n }));\n\n const response = await request(app)\n .post(VERIFY_EMAIL_URL)\n .set('Authorization', 'Bearer valid-token')\n .send({ code });\n\n expect(response.status).toBe(200);\n expect(response.body.emailVerified).toBe(true);\n expect(prismaMock.merchant.update).toHaveBeenCalledWith({\n where: { id: 'uuid-1' },\n data: {\n emailVerified: true,\n emailOtp: null,\n emailOtpExpiresAt: null,\n },\n });\n });\n\n test('returns 400 for wrong code', async () => {\n const emailOtp = await bcrypt.hash('123456', 10);\n const merchantWithOtp = {\n ...registeredMerchant,\n emailOtp,\n emailOtpExpiresAt: new Date('2026-06-21T12:05:00.000Z'),\n };\n\n authenticateAs(merchantWithOtp);\n prismaMock.merchant.findUnique.mockResolvedValue(merchantWithOtp as any);\n\n const response = await request(app)\n .post(VERIFY_EMAIL_URL)\n .set('Authorization', 'Bearer valid-token')\n .send({ code: '654321' });\n\n expect(response.status).toBe(400);\n expect(response.body).toEqual({ error: 'Invalid verification code' });\n });\n\n test('returns 400 with Code expired for expired code', async () => {\n const code = '123456';\n const emailOtp = await bcrypt.hash(code, 10);\n const merchantWithOtp = {\n ...registeredMerchant,\n emailOtp,\n emailOtpExpiresAt: new Date('2026-06-21T11:59:00.000Z'),\n };\n\n authenticateAs(merchantWithOtp);\n prismaMock.merchant.findUnique.mockResolvedValue(merchantWithOtp as any);\n\n const response = await request(app)\n .post(VERIFY_EMAIL_URL)\n .set('Authorization', 'Bearer valid-token')\n .send({ code });\n\n expect(response.status).toBe(400);\n expect(response.body).toEqual({ error: 'Code expired' });\n });\n });\n\n describe('POST /api/v1/auth/resend-otp', () => {\n test('returns 401 for unauthenticated requests', async () => {\n const response = await request(app).post(RESEND_OTP_URL);\n\n expect(response.status).toBe(401);\n expect(response.body).toEqual({ error: 'Authentication required' });\n });\n\n test('returns 200 and re-sends OTP when cooldown has elapsed', async () => {\n const merchantWithOtp = {\n ...registeredMerchant,\n emailOtp: 'hashed',\n emailOtpExpiresAt: new Date('2026-06-21T11:58:00.000Z'),\n };\n\n authenticateAs(merchantWithOtp);\n prismaMock.merchant.findUnique.mockResolvedValue(merchantWithOtp as any);\n prismaMock.merchant.update.mockResolvedValue(merchantWithOtp as any);\n\n const response = await request(app)\n .post(RESEND_OTP_URL)\n .set('Authorization', 'Bearer valid-token');\n\n expect(response.status).toBe(200);\n expect(response.body).toEqual({ message: 'Verification code sent' });\n expect(sendOtpMock).toHaveBeenCalledWith(\n 'ada@example.com',\n expect.stringMatching(/^\\d{6}$/),\n 'Ada',\n );\n expect(prismaMock.merchant.update).toHaveBeenCalledWith({\n where: { id: 'uuid-1' },\n data: {\n emailOtp: expect.any(String),\n emailOtpExpiresAt: expect.any(Date),\n },\n });\n });\n\n test('returns 429 when resend is requested within one minute', async () => {\n const merchantWithOtp = {\n ...registeredMerchant,\n emailOtp: 'hashed',\n emailOtpExpiresAt: new Date('2026-06-21T12:09:30.000Z'),\n };\n\n authenticateAs(merchantWithOtp);\n prismaMock.merchant.findUnique.mockResolvedValue(merchantWithOtp as any);\n\n const response = await request(app)\n .post(RESEND_OTP_URL)\n .set('Authorization', 'Bearer valid-token');\n\n expect(response.status).toBe(429);\n expect(response.body).toEqual({ error: 'Please wait before requesting a new code' });\n expect(sendOtpMock).not.toHaveBeenCalled();\n });\n });\n});\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\integration\\auth.middleware.test.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/no-unused-vars", - "severity": 2, - "message": "'jest' is defined but never used. Allowed unused vars must match /^_/u.", - "line": 1, - "column": 10, - "nodeType": null, - "messageId": "unusedVar", - "endLine": 1, - "endColumn": 14 - } - ], - "suppressedMessages": [], - "errorCount": 1, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import { jest } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\nimport jwt from 'jsonwebtoken';\nimport request from 'supertest';\nimport {\n TEST_INTEGRATION_PREFIX,\n TEST_INTEGRATION_RAW_API_KEY,\n TEST_UNKNOWN_RAW_API_KEY,\n testApiKeyRegex,\n} from '../helpers/api-key.fixtures.js';\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { default: app } = await import('../../src/app.js');\nconst { environment } = await import('../../src/config/environment.js');\nconst { hashApiKey } = await import('../../src/utils/api-key.utils.js');\n\nconst merchant = {\n id: 'merchant-1',\n merchantId: 1,\n address: '0x123',\n account: null,\n email: 'merchant@example.com',\n firstName: 'Ada',\n lastName: 'Lovelace',\n businessName: 'Engines',\n category: 'software',\n description: 'desc',\n logo: null,\n webhook: null,\n active: true,\n verified: false,\n emailVerified: true,\n registered: true,\n emailOtp: null,\n emailOtpExpiresAt: null,\n createdAt: new Date('2026-06-27T12:00:00.000Z'),\n updatedAt: new Date('2026-06-27T12:00:00.000Z'),\n};\n\nconst rawApiKey = TEST_INTEGRATION_RAW_API_KEY;\nconst apiKeyRecord = {\n id: 'key-1',\n merchantId: merchant.id,\n keyHash: hashApiKey(rawApiKey),\n prefix: TEST_INTEGRATION_PREFIX,\n name: 'Integration',\n lastUsedAt: null,\n expiresAt: null,\n revokedAt: null,\n createdAt: new Date('2026-06-27T12:00:00.000Z'),\n};\n\ndescribe('authenticateMerchant auth paths', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n });\n\n test('accepts valid refresh session tokens', async () => {\n prismaMock.refreshToken.findUnique.mockResolvedValue({\n id: 'session-1',\n merchantId: merchant.id,\n token: 'valid-session-token',\n expiresAt: new Date(Date.now() + 60 * 60 * 1000),\n createdAt: new Date(),\n merchant,\n } as any);\n prismaMock.apiKey.findMany.mockResolvedValue([]);\n\n const response = await request(app)\n .get('/api/v1/merchants/api-keys')\n .set('Authorization', 'Bearer valid-session-token');\n\n expect(response.status).toBe(200);\n expect(prismaMock.refreshToken.findUnique).toHaveBeenCalled();\n expect(prismaMock.apiKey.findUnique).not.toHaveBeenCalled();\n });\n\n test('accepts valid JWT access tokens', async () => {\n const accessToken = jwt.sign(\n { sub: merchant.id, address: merchant.address },\n environment.jwtSecret,\n {\n expiresIn: '15m',\n },\n );\n prismaMock.merchant.findUnique.mockResolvedValue(merchant as any);\n prismaMock.apiKey.findMany.mockResolvedValue([]);\n\n const response = await request(app)\n .get('/api/v1/merchants/api-keys')\n .set('Authorization', `Bearer ${accessToken}`);\n\n expect(response.status).toBe(200);\n expect(prismaMock.merchant.findUnique).toHaveBeenCalledWith({ where: { id: merchant.id } });\n expect(prismaMock.refreshToken.findUnique).not.toHaveBeenCalled();\n });\n\n test('accepts valid API keys on merchant routes and updates lastUsedAt', async () => {\n prismaMock.apiKey.findUnique.mockResolvedValue({\n ...apiKeyRecord,\n merchant,\n } as any);\n prismaMock.apiKey.update.mockResolvedValue(apiKeyRecord as any);\n prismaMock.invoice.findMany.mockResolvedValue([]);\n\n const response = await request(app)\n .get('/api/v1/invoices')\n .set('Authorization', `Bearer ${rawApiKey}`);\n\n expect(response.status).toBe(200);\n expect(prismaMock.apiKey.update).toHaveBeenCalledWith({\n where: { id: 'key-1' },\n data: { lastUsedAt: expect.any(Date) },\n });\n });\n\n test('rejects API keys on key-management routes', async () => {\n prismaMock.apiKey.findUnique.mockResolvedValue({\n ...apiKeyRecord,\n merchant,\n } as any);\n prismaMock.apiKey.update.mockResolvedValue(apiKeyRecord as any);\n prismaMock.apiKey.findMany.mockResolvedValue([apiKeyRecord] as any);\n\n const response = await request(app)\n .get('/api/v1/merchants/api-keys')\n .set('Authorization', `Bearer ${rawApiKey}`);\n\n expect(response.status).toBe(401);\n expect(prismaMock.apiKey.findMany).not.toHaveBeenCalled();\n });\n\n test('returns 401 for unknown API keys', async () => {\n prismaMock.apiKey.findUnique.mockResolvedValue(null);\n\n const response = await request(app)\n .get('/api/v1/merchants/api-keys')\n .set('Authorization', `Bearer ${TEST_UNKNOWN_RAW_API_KEY}`);\n\n expect(response.status).toBe(401);\n });\n\n test('returns 401 for expired API keys', async () => {\n prismaMock.apiKey.findUnique.mockResolvedValue({\n ...apiKeyRecord,\n expiresAt: new Date('2020-01-01T00:00:00.000Z'),\n merchant,\n } as any);\n\n const response = await request(app)\n .get('/api/v1/merchants/api-keys')\n .set('Authorization', `Bearer ${rawApiKey}`);\n\n expect(response.status).toBe(401);\n expect(prismaMock.apiKey.update).not.toHaveBeenCalled();\n });\n\n test('returns 401 when Authorization header is missing', async () => {\n const response = await request(app).get('/api/v1/merchants/api-keys');\n\n expect(response.status).toBe(401);\n });\n\n test('returns 401 when bearer token is empty', async () => {\n const response = await request(app)\n .get('/api/v1/merchants/api-keys')\n .set('Authorization', 'Bearer ');\n\n expect(response.status).toBe(401);\n });\n});\n\ndescribe('API key management security', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n prismaMock.$transaction.mockImplementation(\n async (callback: (tx: typeof prismaMock) => unknown) => callback(prismaMock),\n );\n });\n\n test('POST stores only hash in database, never raw key', async () => {\n prismaMock.refreshToken.findUnique.mockResolvedValue({\n id: 'session-1',\n merchantId: merchant.id,\n token: 'valid-session-token',\n expiresAt: new Date(Date.now() + 60 * 60 * 1000),\n createdAt: new Date(),\n merchant,\n } as any);\n prismaMock.apiKey.count.mockResolvedValue(0);\n prismaMock.apiKey.create.mockImplementation(async (args: any) => ({\n id: 'key-new',\n merchantId: merchant.id,\n keyHash: args.data.keyHash,\n prefix: args.data.prefix,\n name: args.data.name,\n lastUsedAt: null,\n expiresAt: null,\n revokedAt: null,\n createdAt: new Date('2026-06-27T13:00:00.000Z'),\n }));\n\n const response = await request(app)\n .post('/api/v1/merchants/api-keys')\n .set('Authorization', 'Bearer valid-session-token')\n .send({ label: 'Server' });\n\n expect(response.status).toBe(201);\n expect(response.body.key).toMatch(testApiKeyRegex);\n\n const createArgs = prismaMock.apiKey.create.mock.calls[0][0];\n expect(createArgs.data.keyHash).toBe(hashApiKey(response.body.key));\n expect(createArgs.data.keyHash).toHaveLength(64);\n expect(createArgs.data).not.toHaveProperty('key');\n expect(JSON.stringify(createArgs.data)).not.toContain(response.body.key);\n });\n\n test('GET scopes keys to authenticated merchant', async () => {\n prismaMock.refreshToken.findUnique.mockResolvedValue({\n id: 'session-1',\n merchantId: merchant.id,\n token: 'valid-session-token',\n expiresAt: new Date(Date.now() + 60 * 60 * 1000),\n createdAt: new Date(),\n merchant,\n } as any);\n prismaMock.apiKey.findMany.mockResolvedValue([]);\n\n await request(app)\n .get('/api/v1/merchants/api-keys')\n .set('Authorization', 'Bearer valid-session-token');\n\n expect(prismaMock.apiKey.findMany).toHaveBeenCalledWith({\n where: { merchantId: merchant.id, revokedAt: null },\n orderBy: { createdAt: 'desc' },\n select: {\n id: true,\n prefix: true,\n name: true,\n lastUsedAt: true,\n createdAt: true,\n },\n });\n });\n\n test('DELETE returns 400 when key is already revoked', async () => {\n prismaMock.refreshToken.findUnique.mockResolvedValue({\n id: 'session-1',\n merchantId: merchant.id,\n token: 'valid-session-token',\n expiresAt: new Date(Date.now() + 60 * 60 * 1000),\n createdAt: new Date(),\n merchant,\n } as any);\n prismaMock.apiKey.findFirst.mockResolvedValue({\n ...apiKeyRecord,\n revokedAt: new Date('2026-06-27T11:00:00.000Z'),\n } as any);\n\n const response = await request(app)\n .delete('/api/v1/merchants/api-keys/key-1')\n .set('Authorization', 'Bearer valid-session-token');\n\n expect(response.status).toBe(400);\n expect(response.body).toEqual({ error: 'API key already revoked' });\n expect(prismaMock.apiKey.update).not.toHaveBeenCalled();\n });\n\n test('revoked key cannot authenticate subsequent requests', async () => {\n prismaMock.apiKey.findUnique.mockResolvedValue({\n ...apiKeyRecord,\n revokedAt: new Date('2026-06-27T14:00:00.000Z'),\n merchant,\n } as any);\n\n const response = await request(app)\n .get('/api/v1/invoices')\n .set('Authorization', `Bearer ${rawApiKey}`);\n\n expect(response.status).toBe(401);\n });\n\n test('API key cannot create additional API keys', async () => {\n prismaMock.apiKey.findUnique.mockResolvedValue({\n ...apiKeyRecord,\n merchant,\n } as any);\n\n const response = await request(app)\n .post('/api/v1/merchants/api-keys')\n .set('Authorization', `Bearer ${rawApiKey}`)\n .send({ label: 'Secondary' });\n\n expect(response.status).toBe(401);\n expect(prismaMock.apiKey.create).not.toHaveBeenCalled();\n expect(prismaMock.refreshToken.findUnique).not.toHaveBeenCalled();\n });\n});\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\integration\\auth.routes.test.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 10, - "column": 5, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 10, - "endColumn": 20 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 15, - "column": 9, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 15, - "endColumn": 17 - } - ], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 2, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import { jest, beforeEach } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\nimport request from 'supertest';\n\nconst mockVerify = { returns: true };\nconst mockKeypairError = { throws: false };\n\njest.unstable_mockModule('@stellar/stellar-sdk', () => ({\n Keypair: {\n fromPublicKey: () => {\n if (mockKeypairError.throws) {\n throw new Error('invalid public key');\n }\n return {\n verify: () => mockVerify.returns,\n };\n },\n },\n}));\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { default: app } = await import('../../src/app.js');\n\nconst address = 'GABCDEF123';\nconst nonce = 'nonce-123';\nconst signature = 'deadbeef';\nconst mockDate = new Date('2026-06-21T12:00:00Z');\n\ndescribe('Auth Routes', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n jest.useFakeTimers({ now: mockDate });\n mockVerify.returns = true;\n mockKeypairError.throws = false;\n });\n\n afterEach(() => {\n jest.useRealTimers();\n });\n\n describe('POST /api/v1/auth/verify', () => {\n const mockAuthNonce = {\n id: 'uuid-1',\n address,\n nonce,\n message: `Shade Authentication\\nAddress: ${address}\\nNonce: ${nonce}\\nTimestamp: 2026-06-21T12:00:00.000Z`,\n expiresAt: new Date('2026-06-21T12:05:00.000Z'),\n usedAt: null,\n createdAt: mockDate,\n merchantId: null,\n };\n\n test('should return 200 with tokens for a valid signature (new merchant)', async () => {\n prismaMock.authNonce.findUnique.mockResolvedValue(mockAuthNonce);\n prismaMock.merchant.findFirst.mockResolvedValue(null);\n prismaMock.merchant.create.mockResolvedValue({\n id: 'merchant-uuid',\n merchantId: 123456,\n address,\n account: null,\n email: null,\n firstName: null,\n lastName: null,\n businessName: null,\n category: null,\n description: null,\n logo: null,\n webhook: null,\n active: true,\n verified: false,\n emailVerified: false,\n registered: false,\n createdAt: mockDate,\n updatedAt: mockDate,\n });\n prismaMock.refreshToken.create.mockResolvedValue({\n id: 'session-uuid',\n merchantId: 'merchant-uuid',\n token: 'refresh-uuid',\n expiresAt: new Date('2026-06-28T12:00:00.000Z'),\n createdAt: mockDate,\n });\n prismaMock.authNonce.update.mockResolvedValue(mockAuthNonce);\n\n const response = await request(app)\n .post('/api/v1/auth/verify')\n .send({ address, nonce, signature });\n\n expect(response.status).toBe(200);\n expect(response.body).toMatchObject({\n accessToken: expect.any(String),\n refreshToken: expect.any(String),\n merchant: {\n id: 'merchant-uuid',\n address,\n isRegistered: false,\n },\n });\n });\n\n test('should return 200 with isRegistered: true for an existing merchant with firstName', async () => {\n prismaMock.authNonce.findUnique.mockResolvedValue(mockAuthNonce);\n prismaMock.merchant.findFirst.mockResolvedValue({\n id: 'merchant-uuid',\n merchantId: 123456,\n address,\n account: null,\n email: 'test@merchant.com',\n firstName: 'Jane',\n lastName: 'Doe',\n businessName: 'Acme',\n category: 'retail',\n description: null,\n logo: null,\n webhook: null,\n active: true,\n verified: true,\n emailVerified: true,\n registered: true,\n createdAt: mockDate,\n updatedAt: mockDate,\n });\n prismaMock.refreshToken.create.mockResolvedValue({\n id: 'session-uuid',\n merchantId: 'merchant-uuid',\n token: 'refresh-uuid',\n expiresAt: new Date('2026-06-28T12:00:00.000Z'),\n createdAt: mockDate,\n });\n prismaMock.authNonce.update.mockResolvedValue(mockAuthNonce);\n\n const response = await request(app)\n .post('/api/v1/auth/verify')\n .send({ address, nonce, signature });\n\n expect(response.status).toBe(200);\n expect(response.body.merchant).toMatchObject({\n isRegistered: true,\n });\n });\n\n test('should return 401 for an invalid signature', async () => {\n mockVerify.returns = false;\n prismaMock.authNonce.findUnique.mockResolvedValue(mockAuthNonce);\n\n const response = await request(app)\n .post('/api/v1/auth/verify')\n .send({ address, nonce, signature });\n\n expect(response.status).toBe(401);\n expect(response.body).toMatchObject({ error: 'Signature verification failed' });\n });\n\n test('should return 401 for an expired nonce', async () => {\n jest.setSystemTime(new Date('2026-06-21T12:10:00.000Z'));\n prismaMock.authNonce.findUnique.mockResolvedValue(mockAuthNonce);\n\n const response = await request(app)\n .post('/api/v1/auth/verify')\n .send({ address, nonce, signature });\n\n expect(response.status).toBe(401);\n expect(response.body).toMatchObject({ error: 'Nonce expired' });\n });\n\n test('should return 401 for a replayed (already used) nonce', async () => {\n prismaMock.authNonce.findUnique.mockResolvedValue({\n ...mockAuthNonce,\n usedAt: new Date('2026-06-21T12:01:00.000Z'),\n });\n\n const response = await request(app)\n .post('/api/v1/auth/verify')\n .send({ address, nonce, signature });\n\n expect(response.status).toBe(401);\n expect(response.body).toMatchObject({ error: 'Nonce already used' });\n });\n\n test('should return 400 when required fields are missing', async () => {\n const response = await request(app).post('/api/v1/auth/verify').send({});\n\n expect(response.status).toBe(400);\n });\n\n test('should return 400 when fields are not strings', async () => {\n const response = await request(app)\n .post('/api/v1/auth/verify')\n .send({ address: 123, nonce: true, signature: [] });\n\n expect(response.status).toBe(400);\n });\n\n test('should return 401 when nonce is not found', async () => {\n prismaMock.authNonce.findUnique.mockResolvedValue(null);\n\n const response = await request(app)\n .post('/api/v1/auth/verify')\n .send({ address, nonce, signature });\n\n expect(response.status).toBe(401);\n expect(response.body).toMatchObject({ error: 'Nonce not found' });\n });\n\n test('should return 401 when signing address does not match the nonce address', async () => {\n prismaMock.authNonce.findUnique.mockResolvedValue(mockAuthNonce);\n\n const response = await request(app)\n .post('/api/v1/auth/verify')\n .send({ address: 'GWRONG', nonce, signature });\n\n expect(response.status).toBe(401);\n expect(response.body).toMatchObject({ error: 'Address mismatch' });\n });\n });\n});\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\integration\\invoice.routes.test.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 60, - "column": 25, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 60, - "endColumn": 27 - } - ], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 1, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import { jest } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\nimport jwt from 'jsonwebtoken';\nimport request from 'supertest';\n\nconst sendInvoiceEmailMock = jest.fn(async () => undefined);\n\njest.unstable_mockModule('../../src/services/email.service.js', () => ({\n __esModule: true,\n sendOtp: jest.fn(async () => undefined),\n sendInvoiceEmail: sendInvoiceEmailMock,\n}));\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { environment } = await import('../../src/config/environment.js');\nconst { default: app } = await import('../../src/app.js');\n\nconst MERCHANT_ID = 'merchant-1';\n\nconst merchant = {\n id: MERCHANT_ID,\n merchantId: 1,\n address: '0x123',\n account: null,\n email: 'merchant@example.com',\n firstName: null,\n lastName: null,\n businessName: null,\n category: null,\n description: null,\n logo: null,\n webhook: null,\n active: true,\n verified: false,\n emailVerified: false,\n registered: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n};\n\nconst baseInvoice = {\n id: 'invoice-1',\n invoiceId: null,\n paymentSlug: 'aZ09-_slug',\n description: 'Website design',\n amount: 5000n,\n token: 'USDC',\n merchantId: MERCHANT_ID,\n status: 'PENDING',\n ref: null,\n payer: null,\n payerEmail: null,\n email: null,\n expiresAt: null,\n datePaid: null,\n createdAt: new Date('2026-01-01T00:00:00.000Z'),\n updatedAt: new Date('2026-01-01T00:00:00.000Z'),\n};\n\nconst authenticate = () => {\n prismaMock.merchant.findUnique.mockResolvedValue(merchant as any);\n};\n\nconst accessToken = jwt.sign(\n { sub: MERCHANT_ID, address: merchant.address },\n environment.jwtSecret,\n);\nconst auth = { Authorization: `Bearer ${accessToken}` };\n\ndescribe('Invoice routes', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n sendInvoiceEmailMock.mockClear();\n });\n\n describe('POST /api/v1/invoices', () => {\n test('returns 401 when unauthenticated', async () => {\n const response = await request(app)\n .post('/api/v1/invoices')\n .send({ description: 'x', amount: '100', token: 'USDC' });\n\n expect(response.status).toBe(401);\n expect(prismaMock.invoice.create).not.toHaveBeenCalled();\n });\n\n test('returns 201 with a unique url-safe paymentSlug', async () => {\n authenticate();\n prismaMock.invoice.create.mockImplementation(async (args: any) => ({\n ...baseInvoice,\n ...args.data,\n }));\n\n const response = await request(app)\n .post('/api/v1/invoices')\n .set(auth)\n .send({ description: 'Website design', amount: '5000', token: 'USDC' });\n\n expect(response.status).toBe(201);\n expect(response.body.status).toBe('PENDING');\n expect(response.body.amount).toBe('5000');\n expect(response.body.paymentSlug).toMatch(/^[A-Za-z0-9_-]+$/);\n });\n\n test('creates a DRAFT invoice when isDraft is true', async () => {\n authenticate();\n prismaMock.invoice.create.mockImplementation(async (args: any) => ({\n ...baseInvoice,\n ...args.data,\n }));\n\n const response = await request(app)\n .post('/api/v1/invoices')\n .set(auth)\n .send({ description: 'Draft', amount: '5000', token: 'USDC', isDraft: true });\n\n expect(response.status).toBe(201);\n expect(response.body.status).toBe('DRAFT');\n });\n\n test('returns 400 when amount is not positive or token is empty', async () => {\n authenticate();\n\n const response = await request(app)\n .post('/api/v1/invoices')\n .set(auth)\n .send({ description: 'x', amount: -5, token: '' });\n\n expect(response.status).toBe(400);\n expect(response.body.errors).toMatchObject({\n amount: expect.any(String),\n token: expect.any(String),\n });\n expect(prismaMock.invoice.create).not.toHaveBeenCalled();\n });\n });\n\n describe('GET /api/v1/invoices', () => {\n test('returns a paginated list scoped to the merchant with status filter', async () => {\n authenticate();\n prismaMock.invoice.findMany.mockResolvedValue([baseInvoice] as any);\n prismaMock.invoice.count.mockResolvedValue(1 as any);\n\n const response = await request(app)\n .get('/api/v1/invoices?status=PENDING&limit=10&offset=0')\n .set(auth);\n\n expect(response.status).toBe(200);\n expect(response.body.data).toHaveLength(1);\n expect(response.body.pagination).toEqual({ limit: 10, offset: 0, total: 1 });\n\n const findArgs = prismaMock.invoice.findMany.mock.calls[0][0];\n expect(findArgs.where).toMatchObject({ merchantId: MERCHANT_ID, status: 'PENDING' });\n });\n\n test('clamps limit to the maximum of 100', async () => {\n authenticate();\n prismaMock.invoice.findMany.mockResolvedValue([] as any);\n prismaMock.invoice.count.mockResolvedValue(0 as any);\n\n const response = await request(app).get('/api/v1/invoices?limit=500').set(auth);\n\n expect(response.status).toBe(200);\n expect(response.body.pagination.limit).toBe(100);\n });\n });\n\n describe('GET /api/v1/invoices/:id', () => {\n test('returns 200 when the invoice belongs to the merchant', async () => {\n authenticate();\n prismaMock.invoice.findFirst.mockResolvedValue(baseInvoice as any);\n\n const response = await request(app).get('/api/v1/invoices/invoice-1').set(auth);\n\n expect(response.status).toBe(200);\n expect(response.body.id).toBe('invoice-1');\n });\n\n test('returns 404 when the invoice is missing or owned by another merchant', async () => {\n authenticate();\n prismaMock.invoice.findFirst.mockResolvedValue(null);\n\n const response = await request(app).get('/api/v1/invoices/other').set(auth);\n\n expect(response.status).toBe(404);\n });\n });\n\n describe('PATCH /api/v1/invoices/:id/void', () => {\n test('voids a PENDING invoice', async () => {\n authenticate();\n prismaMock.invoice.findFirst.mockResolvedValue(baseInvoice as any);\n prismaMock.invoice.update.mockResolvedValue({\n ...baseInvoice,\n status: 'CANCELLED',\n } as any);\n\n const response = await request(app).patch('/api/v1/invoices/invoice-1/void').set(auth);\n\n expect(response.status).toBe(200);\n expect(response.body.status).toBe('CANCELLED');\n });\n\n test('returns 400 when voiding a non-PENDING invoice', async () => {\n authenticate();\n prismaMock.invoice.findFirst.mockResolvedValue({\n ...baseInvoice,\n status: 'PAID',\n } as any);\n\n const response = await request(app).patch('/api/v1/invoices/invoice-1/void').set(auth);\n\n expect(response.status).toBe(400);\n expect(prismaMock.invoice.update).not.toHaveBeenCalled();\n });\n });\n\n describe('GET /api/v1/invoices/:id/pdf', () => {\n test('returns 401 when unauthenticated', async () => {\n const response = await request(app).get('/api/v1/invoices/invoice-1/pdf');\n\n expect(response.status).toBe(401);\n });\n\n test('returns 404 when the invoice is missing or owned by another merchant', async () => {\n authenticate();\n prismaMock.invoice.findFirst.mockResolvedValue(null);\n\n const response = await request(app).get('/api/v1/invoices/other/pdf').set(auth);\n\n expect(response.status).toBe(404);\n });\n\n test('streams a real PDF scoped to the authenticated merchant, never touching disk', async () => {\n authenticate();\n prismaMock.invoice.findFirst.mockResolvedValue({ ...baseInvoice, merchant } as any);\n\n const response = await request(app).get('/api/v1/invoices/invoice-1/pdf').set(auth);\n\n expect(response.status).toBe(200);\n expect(response.headers['content-type']).toContain('application/pdf');\n expect(response.headers['content-disposition']).toBe(\n `attachment; filename=\"invoice-${baseInvoice.paymentSlug}.pdf\"`,\n );\n const body = response.body as Buffer;\n expect(Buffer.isBuffer(body)).toBe(true);\n expect(body.subarray(0, 5).toString('ascii')).toBe('%PDF-');\n\n const findArgs = prismaMock.invoice.findFirst.mock.calls[0][0];\n expect(findArgs.where).toMatchObject({ id: 'invoice-1', merchantId: MERCHANT_ID });\n });\n });\n\n describe('POST /api/v1/invoices/:id/send', () => {\n test('returns 401 when unauthenticated', async () => {\n const response = await request(app).post('/api/v1/invoices/invoice-1/send');\n\n expect(response.status).toBe(401);\n expect(sendInvoiceEmailMock).not.toHaveBeenCalled();\n });\n\n test('returns 404 when the invoice is missing or owned by another merchant', async () => {\n authenticate();\n prismaMock.invoice.findFirst.mockResolvedValue(null);\n\n const response = await request(app).post('/api/v1/invoices/other/send').set(auth);\n\n expect(response.status).toBe(404);\n expect(sendInvoiceEmailMock).not.toHaveBeenCalled();\n });\n\n test('returns 400 and does not attempt to send when the invoice has no email set', async () => {\n authenticate();\n prismaMock.invoice.findFirst.mockResolvedValue({\n ...baseInvoice,\n email: null,\n merchant,\n } as any);\n\n const response = await request(app).post('/api/v1/invoices/invoice-1/send').set(auth);\n\n expect(response.status).toBe(400);\n expect(sendInvoiceEmailMock).not.toHaveBeenCalled();\n });\n\n test('sends the invoice email when invoice.email is set', async () => {\n authenticate();\n const invoiceWithEmail = { ...baseInvoice, email: 'payer@example.com', merchant };\n prismaMock.invoice.findFirst.mockResolvedValue(invoiceWithEmail as any);\n\n const response = await request(app).post('/api/v1/invoices/invoice-1/send').set(auth);\n\n expect(response.status).toBe(200);\n expect(sendInvoiceEmailMock).toHaveBeenCalledWith(invoiceWithEmail, merchant);\n });\n });\n});\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\integration\\merchant.profile.test.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/no-unused-vars", - "severity": 2, - "message": "'jest' is defined but never used. Allowed unused vars must match /^_/u.", - "line": 1, - "column": 10, - "nodeType": null, - "messageId": "unusedVar", - "endLine": 1, - "endColumn": 14 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 34, - "column": 60, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 34, - "endColumn": 62 - } - ], - "suppressedMessages": [], - "errorCount": 1, - "fatalErrorCount": 0, - "warningCount": 1, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import { jest } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\nimport request from 'supertest';\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { default: app } = await import('../../src/app.js');\n\nconst ME_URL = '/api/v1/merchants/me';\n\nconst baseMerchant = {\n id: 'uuid-1',\n merchantId: 1,\n address: '0x123',\n account: 'CCONTRACT',\n email: 'ada@example.com',\n firstName: 'Ada',\n lastName: 'Lovelace',\n businessName: 'Analytical Engines',\n category: 'software',\n description: 'We build computing machines.',\n logo: null,\n webhook: null,\n active: true,\n verified: false,\n emailVerified: false,\n registered: true,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n // Internal relations that the sanitizer allow-list must strip from responses.\n refreshTokens: [{ id: 'rt-1', token: 'secret-token' }],\n apiKeys: [{ id: 'ak-1', keyHash: 'hashed-secret' }],\n};\n\nconst authenticateAs = (merchant: Record) => {\n prismaMock.refreshToken.findUnique.mockResolvedValue({\n id: 'session-1',\n merchantId: merchant.id,\n token: 'valid-token',\n expiresAt: new Date(Date.now() + 60 * 60 * 1000),\n createdAt: new Date(),\n merchant,\n } as any);\n};\n\ndescribe('GET /api/v1/merchants/me', () => {\n beforeEach(() => mockReset(prismaMock));\n\n test('returns 401 when unauthenticated', async () => {\n const response = await request(app).get(ME_URL);\n expect(response.status).toBe(401);\n });\n\n test('returns 200 with the full profile and no internal fields', async () => {\n authenticateAs(baseMerchant);\n prismaMock.merchant.findUnique.mockResolvedValue(baseMerchant as any);\n\n const response = await request(app).get(ME_URL).set('Authorization', 'Bearer valid-token');\n\n expect(response.status).toBe(200);\n expect(response.body).toMatchObject({ id: 'uuid-1', account: 'CCONTRACT', webhook: null });\n expect(response.body).not.toHaveProperty('refreshTokens');\n expect(response.body).not.toHaveProperty('apiKeys');\n });\n});\n\ndescribe('PATCH /api/v1/merchants/me', () => {\n beforeEach(() => mockReset(prismaMock));\n\n test('returns 401 when unauthenticated', async () => {\n const response = await request(app).patch(ME_URL).send({ firstName: 'Grace' });\n expect(response.status).toBe(401);\n expect(prismaMock.merchant.update).not.toHaveBeenCalled();\n });\n\n test('updates a valid partial payload and returns 200', async () => {\n authenticateAs(baseMerchant);\n prismaMock.merchant.update.mockImplementation(async (args: any) => ({\n ...baseMerchant,\n ...args.data,\n }));\n\n const response = await request(app)\n .patch(ME_URL)\n .set('Authorization', 'Bearer valid-token')\n .send({ firstName: 'Grace', webhook: 'https://example.com/hook' });\n\n expect(response.status).toBe(200);\n expect(response.body).toMatchObject({\n firstName: 'Grace',\n webhook: 'https://example.com/hook',\n });\n });\n\n test('silently ignores non-editable fields (address/email/merchantId/account)', async () => {\n authenticateAs(baseMerchant);\n prismaMock.merchant.update.mockImplementation(async (args: any) => ({\n ...baseMerchant,\n ...args.data,\n }));\n\n const response = await request(app)\n .patch(ME_URL)\n .set('Authorization', 'Bearer valid-token')\n .send({\n firstName: 'Grace',\n address: '0xHACK',\n email: 'evil@example.com',\n merchantId: 999,\n account: '0xHACKED',\n });\n\n expect(response.status).toBe(200);\n const updateArg = prismaMock.merchant.update.mock.calls[0][0];\n expect(updateArg.data).toEqual({ firstName: 'Grace' });\n expect(response.body.address).toBe('0x123');\n expect(response.body.email).toBe('ada@example.com');\n expect(response.body.merchantId).toBe(1);\n expect(response.body.account).toBe('CCONTRACT');\n });\n\n test('returns 400 for an invalid (non-HTTPS) webhook', async () => {\n authenticateAs(baseMerchant);\n\n const response = await request(app)\n .patch(ME_URL)\n .set('Authorization', 'Bearer valid-token')\n .send({ webhook: 'http://example.com/hook' });\n\n expect(response.status).toBe(400);\n expect(response.body.error).toBe('Validation failed');\n expect(prismaMock.merchant.update).not.toHaveBeenCalled();\n });\n\n test('clears the webhook when sent null', async () => {\n authenticateAs(baseMerchant);\n prismaMock.merchant.update.mockImplementation(async (args: any) => ({\n ...baseMerchant,\n ...args.data,\n }));\n\n const response = await request(app)\n .patch(ME_URL)\n .set('Authorization', 'Bearer valid-token')\n .send({ webhook: null });\n\n expect(response.status).toBe(200);\n expect(response.body.webhook).toBeNull();\n });\n\n test('returns 400 for a required text field sent empty', async () => {\n authenticateAs(baseMerchant);\n\n const response = await request(app)\n .patch(ME_URL)\n .set('Authorization', 'Bearer valid-token')\n .send({ firstName: '' });\n\n expect(response.status).toBe(400);\n expect(prismaMock.merchant.update).not.toHaveBeenCalled();\n });\n\n test('returns 400 for an empty payload', async () => {\n authenticateAs(baseMerchant);\n\n const response = await request(app)\n .patch(ME_URL)\n .set('Authorization', 'Bearer valid-token')\n .send({});\n\n expect(response.status).toBe(400);\n expect(prismaMock.merchant.update).not.toHaveBeenCalled();\n });\n});\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\integration\\merchant.register.test.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 16, - "column": 3, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 16, - "endColumn": 16 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 17, - "column": 3, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 17, - "endColumn": 18 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 18, - "column": 3, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 18, - "endColumn": 24 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 28, - "column": 54, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 28, - "endColumn": 56 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 65, - "column": 60, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 65, - "endColumn": 62 - } - ], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 5, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import { jest } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\nimport jwt from 'jsonwebtoken';\nimport request from 'supertest';\n\nconst sendOtpMock = jest.fn(async () => undefined);\n\njest.unstable_mockModule('../../src/services/email.service.js', () => ({\n __esModule: true,\n sendOtp: sendOtpMock,\n sendInvoiceEmail: jest.fn(async () => undefined),\n}));\n\njest.unstable_mockModule('../../src/services/otp.services.js', () => ({\n __esModule: true,\n generateOtp: () => '123456',\n hashOtp: async () => 'hashed-otp',\n verifyOtpHash: async () => true,\n issueEmailOtp: jest.fn(),\n verifyEmailOtp: jest.fn(),\n resendEmailOtp: jest.fn(),\n}));\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { environment } = await import('../../src/config/environment.js');\nconst { default: app } = await import('../../src/app.js');\n\nconst tokenFor = (merchant: Record) =>\n jwt.sign({ sub: merchant.id as string }, environment.jwtSecret);\n\nconst REGISTER_URL = '/api/v1/merchants/register';\n\nconst baseMerchant = {\n id: 'uuid-1',\n merchantId: 1,\n address: '0x123',\n account: null,\n email: null,\n firstName: null,\n lastName: null,\n businessName: null,\n category: null,\n description: null,\n logo: null,\n webhook: null,\n active: true,\n verified: false,\n emailVerified: false,\n registered: false,\n emailOtp: null,\n emailOtpExpiresAt: null,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n};\n\nconst validPayload = {\n firstName: 'Ada',\n lastName: 'Lovelace',\n email: 'ada@example.com',\n businessName: 'Analytical Engines',\n category: 'software',\n description: 'We build computing machines.',\n};\n\nconst authenticateAs = (merchant: Record) => {\n prismaMock.merchant.findUnique.mockResolvedValue(merchant as any);\n};\n\nconst authHeader = `Bearer ${tokenFor(baseMerchant)}`;\n\ndescribe('POST /api/v1/merchants/register', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n sendOtpMock.mockClear();\n });\n\n test('returns 401 for unauthenticated requests', async () => {\n const response = await request(app).post(REGISTER_URL).send(validPayload);\n\n expect(response.status).toBe(401);\n expect(response.body).toEqual({ error: 'Authentication required' });\n expect(prismaMock.merchant.update).not.toHaveBeenCalled();\n });\n\n test('returns 401 when the token is invalid', async () => {\n prismaMock.refreshToken.findUnique.mockResolvedValue(null);\n\n const response = await request(app)\n .post(REGISTER_URL)\n .set('Authorization', 'Bearer bad-token')\n .send(validPayload);\n\n expect(response.status).toBe(401);\n expect(response.body).toEqual({ error: 'Invalid or expired token' });\n });\n\n test('returns 200 with the merchant profile on valid payload', async () => {\n authenticateAs(baseMerchant);\n prismaMock.merchant.findUnique.mockResolvedValue(baseMerchant as any);\n prismaMock.merchant.findFirst.mockResolvedValue(null);\n prismaMock.merchant.update.mockImplementation(async (args: any) => ({\n ...baseMerchant,\n ...args.data,\n }));\n\n const response = await request(app)\n .post(REGISTER_URL)\n .set('Authorization', authHeader)\n .send(validPayload);\n\n expect(response.status).toBe(200);\n expect(response.body).toMatchObject({\n id: 'uuid-1',\n firstName: 'Ada',\n lastName: 'Lovelace',\n email: 'ada@example.com',\n businessName: 'Analytical Engines',\n emailVerified: false,\n registered: true,\n });\n expect(sendOtpMock).toHaveBeenCalledWith('ada@example.com', '123456', 'Ada');\n });\n\n test('returns 409 when the email is already registered', async () => {\n authenticateAs(baseMerchant);\n prismaMock.merchant.findUnique.mockResolvedValue(baseMerchant as any);\n prismaMock.merchant.findFirst.mockResolvedValue({\n ...baseMerchant,\n id: 'uuid-2',\n } as any);\n\n const response = await request(app)\n .post(REGISTER_URL)\n .set('Authorization', authHeader)\n .send(validPayload);\n\n expect(response.status).toBe(409);\n expect(response.body).toEqual({ error: 'Email already registered' });\n });\n\n test('returns 409 when the merchant already completed registration', async () => {\n const registeredMerchant = { ...baseMerchant, registered: true };\n authenticateAs(registeredMerchant);\n prismaMock.merchant.findUnique.mockResolvedValue(registeredMerchant as any);\n\n const response = await request(app)\n .post(REGISTER_URL)\n .set('Authorization', authHeader)\n .send(validPayload);\n\n expect(response.status).toBe(409);\n expect(response.body).toEqual({ error: 'Profile already set up' });\n });\n\n test('returns 400 with field-level errors when required fields are missing', async () => {\n authenticateAs(baseMerchant);\n\n const response = await request(app)\n .post(REGISTER_URL)\n .set('Authorization', authHeader)\n .send({ email: 'not-an-email' });\n\n expect(response.status).toBe(400);\n expect(response.body.error).toBe('Validation failed');\n expect(response.body.errors).toMatchObject({\n firstName: expect.any(String),\n lastName: expect.any(String),\n email: expect.any(String),\n businessName: expect.any(String),\n category: expect.any(String),\n description: expect.any(String),\n });\n expect(prismaMock.merchant.update).not.toHaveBeenCalled();\n });\n});\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\integration\\merchant.routes.test.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/no-unused-vars", - "severity": 2, - "message": "'jest' is defined but never used. Allowed unused vars must match /^_/u.", - "line": 1, - "column": 10, - "nodeType": null, - "messageId": "unusedVar", - "endLine": 1, - "endColumn": 14 - } - ], - "suppressedMessages": [], - "errorCount": 1, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import { jest } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\nimport request from 'supertest';\n\n// Wait for the mock to be applied\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { default: app } = await import('../../src/app.js');\n\ndescribe('Merchant Routes', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n });\n\n test('POST /api/v1/merchants should create a merchant', async () => {\n const merchantData = {\n merchantId: 1,\n address: '0x123',\n email: 'test@example.com',\n };\n\n const expectedMerchant = {\n id: 'uuid-1',\n ...merchantData,\n active: true,\n verified: false,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n };\n\n prismaMock.merchant.create.mockResolvedValue(expectedMerchant as any);\n\n const response = await request(app).post('/api/v1/merchants').send(merchantData);\n\n expect(response.status).toBe(201);\n expect(response.body).toEqual(expectedMerchant);\n });\n\n test('GET /api/v1/merchants/:id should return a merchant', async () => {\n const expectedMerchant = {\n id: 'uuid-1',\n merchantId: 1,\n address: '0x123',\n email: 'test@example.com',\n active: true,\n verified: false,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n };\n\n prismaMock.merchant.findUnique.mockResolvedValue(expectedMerchant as any);\n\n const response = await request(app).get('/api/v1/merchants/1');\n\n expect(response.status).toBe(200);\n expect(response.body).toEqual(expectedMerchant);\n });\n\n test('GET /api/v1/merchants should list merchants', async () => {\n const merchants = [\n {\n id: 'uuid-1',\n merchantId: 1,\n address: '0x1',\n email: '1',\n active: true,\n verified: false,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n },\n ];\n\n prismaMock.merchant.findMany.mockResolvedValue(merchants as any);\n\n const response = await request(app).get('/api/v1/merchants?limit=10&offset=0');\n\n expect(response.status).toBe(200);\n expect(response.body).toEqual(merchants);\n });\n});\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\integration\\merchant.signing-key.test.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 14, - "column": 5, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 14, - "endColumn": 20 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 15, - "column": 5, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 15, - "endColumn": 17 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 16, - "column": 5, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 16, - "endColumn": 29 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 49, - "column": 36, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 49, - "endColumn": 38 - } - ], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 4, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import { jest } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\nimport request from 'supertest';\nimport { TEST_RAW_API_KEY } from '../helpers/api-key.fixtures.js';\n\njest.unstable_mockModule('../../src/utils/api-key.utils.js', () => {\n const prefix = 'sk_' + 'live_';\n return {\n __esModule: true,\n API_KEY_PREFIX: prefix,\n API_KEY_RANDOM_LENGTH: 32,\n API_KEY_DISPLAY_PREFIX_LENGTH: 8,\n MAX_ACTIVE_API_KEYS: 10,\n isApiKeyToken: (token: string) => token.startsWith(prefix),\n hashApiKey: (value: string) => `hash-${value}`,\n generateApiKeyMaterial: () => ({ rawKey: 'x', prefix: 'x', keyHash: 'x' }),\n };\n});\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { default: app } = await import('../../src/app.js');\n\nconst HEX32 = /^[0-9a-f]{64}$/;\n\nconst merchant = {\n id: 'merchant-1',\n merchantId: 1,\n address: '0x123',\n account: null,\n merchantKey: null,\n email: 'merchant@example.com',\n firstName: 'Ada',\n lastName: 'Lovelace',\n businessName: 'Engines',\n category: 'software',\n description: 'desc',\n logo: null,\n webhook: null,\n active: true,\n verified: false,\n emailVerified: true,\n registered: true,\n emailOtp: null,\n emailOtpExpiresAt: null,\n createdAt: new Date('2026-06-27T12:00:00.000Z'),\n updatedAt: new Date('2026-06-27T12:00:00.000Z'),\n};\n\nconst authenticateWithSession = () => {\n prismaMock.refreshToken.findUnique.mockResolvedValue({\n id: 'session-1',\n merchantId: merchant.id,\n token: 'valid-token',\n expiresAt: new Date(Date.now() + 60 * 60 * 1000),\n createdAt: new Date(),\n merchant,\n } as any);\n};\n\ndescribe('POST /api/v1/merchants/signing-key', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n jest.spyOn(console, 'info').mockImplementation(() => {});\n });\n\n afterEach(() => {\n jest.restoreAllMocks();\n });\n\n test('returns 401 when unauthenticated', async () => {\n const response = await request(app).post('/api/v1/merchants/signing-key');\n\n expect(response.status).toBe(401);\n expect(prismaMock.merchant.update).not.toHaveBeenCalled();\n });\n\n test('returns 401 when authenticated with an API key (session-only)', async () => {\n const response = await request(app)\n .post('/api/v1/merchants/signing-key')\n .set('Authorization', `Bearer ${TEST_RAW_API_KEY}`);\n\n expect(response.status).toBe(401);\n expect(prismaMock.merchant.update).not.toHaveBeenCalled();\n });\n\n test('returns 201 with hex public + private, persisting only the public key', async () => {\n authenticateWithSession();\n prismaMock.merchant.findUnique.mockResolvedValue({ ...merchant });\n prismaMock.merchant.updateMany.mockResolvedValue({ count: 1 });\n\n const response = await request(app)\n .post('/api/v1/merchants/signing-key')\n .set('Authorization', 'Bearer valid-token');\n\n expect(response.status).toBe(201);\n expect(response.body.publicKey).toMatch(HEX32);\n expect(response.body.privateKey).toMatch(HEX32);\n\n const updateArgs = prismaMock.merchant.updateMany.mock.calls[0][0];\n expect(updateArgs.data).toEqual({ merchantKey: response.body.publicKey });\n expect(JSON.stringify(updateArgs)).not.toContain(response.body.privateKey);\n });\n});\n\ndescribe('GET /api/v1/merchants/me merchantKey exposure', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n });\n\n test('returns the public merchantKey and never a private key', async () => {\n authenticateWithSession();\n const publicKey = 'a'.repeat(64);\n prismaMock.merchant.findUnique.mockResolvedValue({ ...merchant, merchantKey: publicKey });\n\n const response = await request(app)\n .get('/api/v1/merchants/me')\n .set('Authorization', 'Bearer valid-token');\n\n expect(response.status).toBe(200);\n expect(response.body.merchantKey).toBe(publicKey);\n expect(response.body).not.toHaveProperty('privateKey');\n });\n});\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\integration\\pay.routes.test.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\jest.setup.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\analytics.schema.test.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\api-key.services.test.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 18, - "column": 5, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 18, - "endColumn": 20 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 19, - "column": 5, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 19, - "endColumn": 17 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 20, - "column": 5, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 20, - "endColumn": 29 - } - ], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 3, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import { jest } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\nimport {\n TEST_KEY_HASH,\n TEST_KEY_PREFIX_DISPLAY,\n TEST_RAW_API_KEY,\n} from '../helpers/api-key.fixtures.js';\n\njest.unstable_mockModule('../../src/utils/api-key.utils.js', () => {\n const prefix = 'sk_' + 'live_';\n const rawKey = `${prefix}testkey1234567890123456789012345`;\n return {\n __esModule: true,\n API_KEY_PREFIX: prefix,\n API_KEY_RANDOM_LENGTH: 32,\n API_KEY_DISPLAY_PREFIX_LENGTH: 8,\n MAX_ACTIVE_API_KEYS: 10,\n isApiKeyToken: (token: string) => token.startsWith(prefix),\n hashApiKey: (rawKeyValue: string) => `hash-${rawKeyValue}`,\n generateApiKeyMaterial: () => ({\n rawKey,\n prefix: `${prefix}testkey1`,\n keyHash: `hash-${rawKey}`,\n }),\n };\n});\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { createApiKey, listApiKeys, revokeApiKey, authenticateApiKey } = await import(\n '../../src/services/api-key.services.js'\n);\n\nconst merchantId = 'merchant-1';\nconst baseApiKeyRecord = {\n id: 'key-1',\n merchantId,\n keyHash: TEST_KEY_HASH,\n prefix: TEST_KEY_PREFIX_DISPLAY,\n name: 'Production',\n lastUsedAt: null,\n expiresAt: null,\n revokedAt: null,\n createdAt: new Date('2026-06-27T12:00:00.000Z'),\n};\n\ndescribe('api-key.services', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n prismaMock.$transaction.mockImplementation(\n async (callback: (tx: typeof prismaMock) => unknown) => callback(prismaMock),\n );\n });\n\n test('createApiKey stores hash and returns raw key once', async () => {\n prismaMock.apiKey.count.mockResolvedValue(0);\n prismaMock.apiKey.create.mockResolvedValue(baseApiKeyRecord as any);\n\n const result = await createApiKey(merchantId, 'Production');\n\n expect(prismaMock.apiKey.create).toHaveBeenCalledWith({\n data: {\n merchantId,\n keyHash: TEST_KEY_HASH,\n prefix: TEST_KEY_PREFIX_DISPLAY,\n name: 'Production',\n },\n });\n expect(result).toMatchObject({\n id: 'key-1',\n key: TEST_RAW_API_KEY,\n prefix: TEST_KEY_PREFIX_DISPLAY,\n label: 'Production',\n });\n });\n\n test('createApiKey rejects when active key limit is reached', async () => {\n prismaMock.apiKey.count.mockResolvedValue(10);\n\n await expect(createApiKey(merchantId)).rejects.toMatchObject({\n statusCode: 400,\n message: 'Maximum of 10 active API keys allowed',\n });\n expect(prismaMock.apiKey.create).not.toHaveBeenCalled();\n });\n\n test('listApiKeys returns non-revoked keys without hashes', async () => {\n prismaMock.apiKey.findMany.mockResolvedValue([baseApiKeyRecord] as any);\n\n const result = await listApiKeys(merchantId);\n\n expect(prismaMock.apiKey.findMany).toHaveBeenCalledWith({\n where: { merchantId, revokedAt: null },\n orderBy: { createdAt: 'desc' },\n select: {\n id: true,\n prefix: true,\n name: true,\n lastUsedAt: true,\n createdAt: true,\n },\n });\n expect(result).toEqual([\n {\n id: 'key-1',\n prefix: TEST_KEY_PREFIX_DISPLAY,\n label: 'Production',\n lastUsedAt: null,\n createdAt: baseApiKeyRecord.createdAt,\n },\n ]);\n expect(result[0]).not.toHaveProperty('keyHash');\n expect(result[0]).not.toHaveProperty('key');\n });\n\n test('revokeApiKey marks key as revoked for owning merchant', async () => {\n prismaMock.apiKey.findFirst.mockResolvedValue(baseApiKeyRecord as any);\n prismaMock.apiKey.update.mockResolvedValue({\n ...baseApiKeyRecord,\n revokedAt: new Date(),\n } as any);\n\n await revokeApiKey(merchantId, 'key-1');\n\n expect(prismaMock.apiKey.findFirst).toHaveBeenCalledWith({\n where: { id: 'key-1', merchantId },\n });\n expect(prismaMock.apiKey.update).toHaveBeenCalledWith({\n where: { id: 'key-1' },\n data: { revokedAt: expect.any(Date) },\n });\n });\n\n test('revokeApiKey returns 404 for another merchant key', async () => {\n prismaMock.apiKey.findFirst.mockResolvedValue(null);\n\n await expect(revokeApiKey('merchant-2', 'key-1')).rejects.toMatchObject({\n statusCode: 404,\n });\n });\n\n test('authenticateApiKey updates lastUsedAt and returns merchant', async () => {\n const merchant = { id: merchantId, merchantId: 1, address: '0x123' };\n prismaMock.apiKey.findUnique.mockResolvedValue({\n ...baseApiKeyRecord,\n merchant,\n } as any);\n prismaMock.apiKey.update.mockResolvedValue(baseApiKeyRecord as any);\n\n const result = await authenticateApiKey(TEST_RAW_API_KEY);\n\n expect(prismaMock.apiKey.findUnique).toHaveBeenCalledWith({\n where: { keyHash: TEST_KEY_HASH },\n include: { merchant: true },\n });\n expect(result).toEqual(merchant);\n expect(prismaMock.apiKey.update).toHaveBeenCalledWith({\n where: { id: 'key-1' },\n data: { lastUsedAt: expect.any(Date) },\n });\n });\n\n test('authenticateApiKey returns null for revoked keys', async () => {\n prismaMock.apiKey.findUnique.mockResolvedValue({\n ...baseApiKeyRecord,\n revokedAt: new Date(),\n merchant: { id: merchantId },\n } as any);\n\n const result = await authenticateApiKey(TEST_RAW_API_KEY);\n\n expect(result).toBeNull();\n expect(prismaMock.apiKey.update).not.toHaveBeenCalled();\n });\n\n test('authenticateApiKey returns null for expired keys', async () => {\n prismaMock.apiKey.findUnique.mockResolvedValue({\n ...baseApiKeyRecord,\n expiresAt: new Date('2020-01-01T00:00:00.000Z'),\n merchant: { id: merchantId },\n } as any);\n\n const result = await authenticateApiKey(TEST_RAW_API_KEY);\n\n expect(result).toBeNull();\n expect(prismaMock.apiKey.update).not.toHaveBeenCalled();\n });\n\n test('revokeApiKey rejects already revoked keys', async () => {\n prismaMock.apiKey.findFirst.mockResolvedValue({\n ...baseApiKeyRecord,\n revokedAt: new Date(),\n } as any);\n\n await expect(revokeApiKey(merchantId, 'key-1')).rejects.toMatchObject({\n statusCode: 400,\n message: 'API key already revoked',\n });\n expect(prismaMock.apiKey.update).not.toHaveBeenCalled();\n });\n\n test('countActiveApiKeys excludes expired but non-revoked keys from limit', async () => {\n prismaMock.apiKey.count.mockResolvedValue(9);\n prismaMock.apiKey.create.mockResolvedValue(baseApiKeyRecord as any);\n\n await createApiKey(merchantId, 'Tenth key');\n\n expect(prismaMock.apiKey.count).toHaveBeenCalledWith({\n where: {\n merchantId,\n revokedAt: null,\n OR: [{ expiresAt: null }, { expiresAt: { gt: expect.any(Date) } }],\n },\n });\n expect(prismaMock.apiKey.create).toHaveBeenCalled();\n });\n});\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\api-key.utils.test.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\auth.middleware.test.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 21, - "column": 21, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 21, - "endColumn": 23 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 28, - "column": 23, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 28, - "endColumn": 25 - } - ], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 2, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import { jest } from '@jest/globals';\nimport jwt from 'jsonwebtoken';\nimport type { Request, Response, NextFunction } from 'express';\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { environment } = await import('../../src/config/environment.js');\nconst { authenticateMerchant } = await import('../../src/middlewares/auth.middleware.js');\n\nconst MERCHANT_ID = 'merchant-1';\n\nconst merchant = {\n id: MERCHANT_ID,\n merchantId: 1,\n address: '0x123',\n registered: true,\n};\n\nconst buildReq = (authorization?: string): Request =>\n ({ headers: authorization ? { authorization } : {} }) as unknown as Request;\n\nconst buildRes = () => {\n const res = {} as Response;\n res.status = jest.fn().mockReturnValue(res) as unknown as Response['status'];\n res.json = jest.fn().mockReturnValue(res) as unknown as Response['json'];\n return res;\n};\n\nconst validToken = () =>\n jwt.sign({ sub: MERCHANT_ID, address: merchant.address }, environment.jwtSecret);\n\ndescribe('authenticateMerchant', () => {\n beforeEach(() => {\n jest.clearAllMocks();\n });\n\n test('attaches the merchant and calls next() for a valid JWT', async () => {\n prismaMock.merchant.findUnique.mockResolvedValue(merchant as any);\n const req = buildReq(`Bearer ${validToken()}`);\n const res = buildRes();\n const next = jest.fn() as unknown as NextFunction;\n\n await authenticateMerchant(req, res, next);\n\n expect(prismaMock.merchant.findUnique).toHaveBeenCalledWith({ where: { id: MERCHANT_ID } });\n expect(req.merchant).toEqual(merchant);\n expect(next).toHaveBeenCalledTimes(1);\n expect(res.status).not.toHaveBeenCalled();\n });\n\n test('returns 401 \"Authentication required\" when the Authorization header is missing', async () => {\n const req = buildReq();\n const res = buildRes();\n const next = jest.fn() as unknown as NextFunction;\n\n await authenticateMerchant(req, res, next);\n\n expect(res.status).toHaveBeenCalledWith(401);\n expect(res.json).toHaveBeenCalledWith({ error: 'Authentication required' });\n expect(next).not.toHaveBeenCalled();\n });\n\n test('returns 401 \"Authentication required\" when the scheme is not Bearer', async () => {\n const req = buildReq('Basic abc123');\n const res = buildRes();\n const next = jest.fn() as unknown as NextFunction;\n\n await authenticateMerchant(req, res, next);\n\n expect(res.status).toHaveBeenCalledWith(401);\n expect(res.json).toHaveBeenCalledWith({ error: 'Authentication required' });\n });\n\n test('returns 401 \"Invalid or expired token\" for a malformed token', async () => {\n const req = buildReq('Bearer not-a-real-jwt');\n const res = buildRes();\n const next = jest.fn() as unknown as NextFunction;\n\n await authenticateMerchant(req, res, next);\n\n expect(res.status).toHaveBeenCalledWith(401);\n expect(res.json).toHaveBeenCalledWith({ error: 'Invalid or expired token' });\n expect(prismaMock.merchant.findUnique).not.toHaveBeenCalled();\n });\n\n test('returns 401 \"Invalid or expired token\" for an expired token', async () => {\n const expired = jwt.sign({ sub: MERCHANT_ID }, environment.jwtSecret, { expiresIn: '-1s' });\n const req = buildReq(`Bearer ${expired}`);\n const res = buildRes();\n const next = jest.fn() as unknown as NextFunction;\n\n await authenticateMerchant(req, res, next);\n\n expect(res.status).toHaveBeenCalledWith(401);\n expect(res.json).toHaveBeenCalledWith({ error: 'Invalid or expired token' });\n });\n\n test('returns 401 \"Invalid or expired token\" when the token is signed with the wrong secret', async () => {\n const forged = jwt.sign({ sub: MERCHANT_ID }, 'a-different-secret');\n const req = buildReq(`Bearer ${forged}`);\n const res = buildRes();\n const next = jest.fn() as unknown as NextFunction;\n\n await authenticateMerchant(req, res, next);\n\n expect(res.status).toHaveBeenCalledWith(401);\n expect(res.json).toHaveBeenCalledWith({ error: 'Invalid or expired token' });\n });\n\n test('returns 401 when the merchant no longer exists in the database', async () => {\n prismaMock.merchant.findUnique.mockResolvedValue(null);\n const req = buildReq(`Bearer ${validToken()}`);\n const res = buildRes();\n const next = jest.fn() as unknown as NextFunction;\n\n await authenticateMerchant(req, res, next);\n\n expect(res.status).toHaveBeenCalledWith(401);\n expect(res.json).toHaveBeenCalledWith({ error: 'Invalid or expired token' });\n expect(next).not.toHaveBeenCalled();\n });\n});\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\auth.services.test.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 9, - "column": 5, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 9, - "endColumn": 20 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 14, - "column": 9, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 14, - "endColumn": 17 - } - ], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 2, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import { jest, beforeEach } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\n\nconst mockVerify = { returns: true };\nconst mockKeypairError = { throws: false };\n\njest.unstable_mockModule('@stellar/stellar-sdk', () => ({\n Keypair: {\n fromPublicKey: () => {\n if (mockKeypairError.throws) {\n throw new Error('invalid public key');\n }\n return {\n verify: () => mockVerify.returns,\n };\n },\n },\n}));\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { environment } = await import('../../src/config/environment.js');\nconst {\n authenticateWallet,\n createNonce,\n verifySignature,\n buildChallengeMessage,\n issueAccessToken,\n issueRefreshToken,\n} = await import('../../src/services/auth.services.js');\n\nconst mockDate = new Date('2026-06-21T12:00:00Z');\n\ndescribe('Auth Services', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n jest.useFakeTimers({ now: mockDate });\n mockVerify.returns = true;\n mockKeypairError.throws = false;\n });\n\n afterEach(() => {\n jest.useRealTimers();\n });\n\n describe('buildChallengeMessage', () => {\n test('should construct the challenge message in a deterministic format', () => {\n const msg = buildChallengeMessage('GABCDEF123', 'nonce-abc', mockDate);\n expect(msg).toBe(\n 'Shade Authentication\\nAddress: GABCDEF123\\nNonce: nonce-abc\\nTimestamp: 2026-06-21T12:00:00.000Z',\n );\n });\n });\n\n describe('createNonce', () => {\n test('should create an AuthNonce record and return nonce, message, and expiresAt', async () => {\n const mockNonce = {\n id: 'uuid-1',\n address: 'GABCDEF123',\n nonce: 'generated-uuid',\n message:\n 'Shade Authentication\\nAddress: GABCDEF123\\nNonce: generated-uuid\\nTimestamp: 2026-06-21T12:00:00.000Z',\n expiresAt: new Date('2026-06-21T12:05:00.000Z'),\n usedAt: null,\n createdAt: mockDate,\n };\n\n prismaMock.authNonce.create.mockResolvedValue(mockNonce);\n\n const result = await createNonce('GABCDEF123');\n\n expect(result).toEqual({\n nonce: mockNonce.nonce,\n message: mockNonce.message,\n expiresAt: mockNonce.expiresAt,\n });\n expect(prismaMock.authNonce.create).toHaveBeenCalledWith({\n data: expect.objectContaining({\n address: 'GABCDEF123',\n nonce: expect.any(String),\n message: expect.any(String),\n expiresAt: expect.any(Date),\n }),\n });\n });\n });\n\n describe('verifySignature', () => {\n const address = 'GABCDEF123';\n const nonce = 'nonce-abc';\n const signature = 'deadbeef';\n const mockAuthNonce = {\n id: 'uuid-1',\n address,\n nonce,\n message: buildChallengeMessage(address, nonce, mockDate),\n expiresAt: new Date('2026-06-21T12:05:00.000Z'),\n usedAt: null,\n createdAt: mockDate,\n merchantId: null,\n };\n\n test('should return valid when signature is correct', async () => {\n prismaMock.authNonce.findUnique.mockResolvedValue(mockAuthNonce);\n\n const result = await verifySignature(address, nonce, signature);\n\n expect(result).toEqual({ valid: true, reason: null });\n expect(prismaMock.authNonce.update).toHaveBeenCalledWith({\n where: { id: 'uuid-1' },\n data: { usedAt: expect.any(Date) },\n });\n });\n\n test('should return invalid when nonce is not found', async () => {\n prismaMock.authNonce.findUnique.mockResolvedValue(null);\n\n const result = await verifySignature(address, nonce, signature);\n\n expect(result).toEqual({ valid: false, reason: 'Nonce not found' });\n });\n\n test('should return invalid when address does not match', async () => {\n prismaMock.authNonce.findUnique.mockResolvedValue(mockAuthNonce);\n\n const result = await verifySignature('GWRONG', nonce, signature);\n\n expect(result).toEqual({ valid: false, reason: 'Address mismatch' });\n });\n\n test('should return invalid when nonce is already used', async () => {\n prismaMock.authNonce.findUnique.mockResolvedValue({\n ...mockAuthNonce,\n usedAt: new Date('2026-06-21T12:01:00.000Z'),\n });\n\n const result = await verifySignature(address, nonce, signature);\n\n expect(result).toEqual({ valid: false, reason: 'Nonce already used' });\n });\n\n test('should return invalid when nonce is expired', async () => {\n jest.setSystemTime(new Date('2026-06-21T12:10:00.000Z'));\n\n prismaMock.authNonce.findUnique.mockResolvedValue(mockAuthNonce);\n\n const result = await verifySignature(address, nonce, signature);\n\n expect(result).toEqual({ valid: false, reason: 'Nonce expired' });\n });\n\n test('should return invalid when signature verification fails', async () => {\n mockVerify.returns = false;\n prismaMock.authNonce.findUnique.mockResolvedValue(mockAuthNonce);\n\n const result = await verifySignature(address, nonce, signature);\n\n expect(result).toEqual({ valid: false, reason: 'Signature verification failed' });\n });\n\n test('should return invalid when address is invalid', async () => {\n mockKeypairError.throws = true;\n prismaMock.authNonce.findUnique.mockResolvedValue(mockAuthNonce);\n\n const result = await verifySignature(address, nonce, signature);\n\n expect(result).toEqual({ valid: false, reason: 'Invalid address or signature format' });\n });\n });\n\n describe('issueAccessToken', () => {\n test('should sign a JWT with sub and address claims', async () => {\n const token = issueAccessToken('merchant-uuid', 'GABCDEF123');\n expect(typeof token).toBe('string');\n expect(token.split('.')).toHaveLength(3);\n\n const jwt = await import('jsonwebtoken');\n const decoded = jwt.default.verify(token, environment.jwtSecret);\n expect(decoded).toMatchObject({\n sub: 'merchant-uuid',\n address: 'GABCDEF123',\n });\n });\n });\n\n describe('issueRefreshToken', () => {\n test('should create a RefreshToken and return the token', async () => {\n prismaMock.refreshToken.create.mockResolvedValue({\n id: 'session-uuid',\n merchantId: 'merchant-uuid',\n token: 'ignored',\n expiresAt: new Date('2026-06-28T12:00:00.000Z'),\n createdAt: mockDate,\n });\n\n const result = await issueRefreshToken('merchant-uuid');\n\n expect(typeof result).toBe('string');\n expect(result.length).toBeGreaterThan(0);\n expect(prismaMock.refreshToken.create).toHaveBeenCalledWith({\n data: {\n merchantId: 'merchant-uuid',\n token: expect.any(String),\n expiresAt: expect.any(Date),\n },\n });\n });\n });\n\n describe('authenticateWallet', () => {\n const address = 'GABCDEF123';\n const nonce = 'nonce-abc';\n const signature = 'deadbeef';\n const mockAuthNonce = {\n id: 'uuid-1',\n address,\n nonce,\n message: buildChallengeMessage(address, nonce, mockDate),\n expiresAt: new Date('2026-06-21T12:05:00.000Z'),\n usedAt: null,\n createdAt: mockDate,\n merchantId: null,\n };\n\n test('should return tokens and merchant on successful auth (new merchant)', async () => {\n prismaMock.authNonce.findUnique.mockResolvedValue(mockAuthNonce);\n prismaMock.merchant.findFirst.mockResolvedValue(null);\n prismaMock.merchant.create.mockResolvedValue({\n id: 'merchant-uuid',\n merchantId: 123456,\n address,\n email: null,\n firstName: null,\n lastName: null,\n businessName: null,\n category: null,\n description: null,\n logo: null,\n active: true,\n verified: false,\n emailVerified: false,\n registered: false,\n createdAt: mockDate,\n updatedAt: mockDate,\n });\n prismaMock.refreshToken.create.mockResolvedValue({\n id: 'session-uuid',\n merchantId: 'merchant-uuid',\n token: 'ignored',\n expiresAt: new Date('2026-06-28T12:00:00.000Z'),\n createdAt: mockDate,\n });\n prismaMock.authNonce.update.mockResolvedValue(mockAuthNonce);\n\n const result = await authenticateWallet(address, nonce, signature);\n\n expect(result.success).toBe(true);\n if (result.success) {\n expect(result.accessToken).toBeTruthy();\n expect(typeof result.refreshToken).toBe('string');\n expect(result.refreshToken.length).toBeGreaterThan(0);\n expect(result.merchant).toEqual({\n id: 'merchant-uuid',\n address,\n isRegistered: false,\n });\n }\n });\n\n test('should return tokens and merchant on successful auth (existing merchant)', async () => {\n prismaMock.authNonce.findUnique.mockResolvedValue(mockAuthNonce);\n prismaMock.merchant.findFirst.mockResolvedValue({\n id: 'existing-merchant-uuid',\n merchantId: 654321,\n address,\n email: 'merchant@test.com',\n firstName: 'John',\n lastName: 'Doe',\n businessName: 'Acme',\n category: 'retail',\n description: 'A merchant',\n logo: null,\n active: true,\n verified: true,\n emailVerified: true,\n registered: true,\n createdAt: mockDate,\n updatedAt: mockDate,\n });\n prismaMock.refreshToken.create.mockResolvedValue({\n id: 'session-uuid',\n merchantId: 'existing-merchant-uuid',\n token: 'ignored',\n expiresAt: new Date('2026-06-28T12:00:00.000Z'),\n createdAt: mockDate,\n });\n prismaMock.authNonce.update.mockResolvedValue(mockAuthNonce);\n\n const result = await authenticateWallet(address, nonce, signature);\n\n expect(result.success).toBe(true);\n if (result.success) {\n expect(typeof result.refreshToken).toBe('string');\n expect(result.merchant).toEqual({\n id: 'existing-merchant-uuid',\n address,\n isRegistered: true,\n });\n }\n });\n\n test('should return failure when signature is invalid', async () => {\n mockVerify.returns = false;\n prismaMock.authNonce.findUnique.mockResolvedValue(mockAuthNonce);\n\n const result = await authenticateWallet(address, nonce, signature);\n\n expect(result.success).toBe(false);\n if (!result.success) {\n expect(result.reason).toBe('Signature verification failed');\n }\n });\n });\n});\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\email.service.resend.test.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\email.service.smtp.test.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\email.service.test.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\indexer.test.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/no-unused-vars", - "severity": 2, - "message": "'setCursor' is assigned a value but never used. Allowed unused vars must match /^_/u.", - "line": 18, - "column": 53, - "nodeType": null, - "messageId": "unusedVar", - "endLine": 18, - "endColumn": 62 - } - ], - "suppressedMessages": [], - "errorCount": 1, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import { jest } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\n\njest.unstable_mockModule('../../src/indexer/sorobanClient.js', () => {\n const mockServer = {\n getLatestLedger: jest.fn(),\n getEvents: jest.fn(),\n };\n return {\n __esModule: true,\n sorobanServer: mockServer,\n default: mockServer,\n };\n});\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { sorobanServer } = (await import('../../src/indexer/sorobanClient.js')) as any;\nconst { tick, startPolling, stopPolling, getCursor, setCursor, resetPoller } = await import(\n '../../src/indexer/poller.js'\n);\nconst { registerEventHandler, clearHandlers, dispatch } = await import(\n '../../src/indexer/registry.js'\n);\nconst { environment } = await import('../../src/config/environment.js');\n\ndescribe('Core Soroban Indexer Infrastructure', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n clearHandlers();\n resetPoller();\n jest.clearAllMocks();\n environment.stellar.contractId = 'C_TEST_CONTRACT_ID';\n environment.stellar.indexerStartLedger = undefined;\n prismaMock.$transaction.mockImplementation(async (cb: any) => cb(prismaMock));\n });\n\n afterEach(() => {\n stopPolling();\n });\n\n it('fails fast if STELLAR_CONTRACT_ID is unset', async () => {\n environment.stellar.contractId = '';\n await expect(tick()).rejects.toThrow(\n 'STELLAR_CONTRACT_ID environment variable is unset or empty',\n );\n await expect(startPolling()).rejects.toThrow(\n 'STELLAR_CONTRACT_ID environment variable is unset or empty',\n );\n });\n\n it('connects to RPC, fetches latest ledger, and logs decoded event without erroring', async () => {\n sorobanServer.getLatestLedger.mockResolvedValue({ sequence: 100 });\n sorobanServer.getEvents.mockResolvedValue({\n events: [\n {\n id: 'evt-1',\n topic: [],\n value: null,\n ledger: 100,\n txHash: 'hash-1',\n },\n ],\n });\n prismaMock.indexerCursor.findUnique.mockResolvedValue(null);\n prismaMock.indexerEvent.findUnique.mockResolvedValue(null);\n\n await tick();\n\n expect(sorobanServer.getLatestLedger).toHaveBeenCalled();\n expect(sorobanServer.getEvents).toHaveBeenCalledWith({\n startLedger: 100,\n filters: [{ type: 'contract', contractIds: ['C_TEST_CONTRACT_ID'] }],\n limit: 100,\n });\n expect(getCursor()).toBe(101);\n });\n\n it('persists cursor after processed batch and resumes correctly', async () => {\n prismaMock.indexerCursor.findUnique.mockResolvedValue({\n contractId: 'C_TEST_CONTRACT_ID',\n lastLedger: 50,\n });\n sorobanServer.getLatestLedger.mockResolvedValue({ sequence: 55 });\n sorobanServer.getEvents.mockResolvedValue({ events: [] });\n\n await tick();\n\n expect(sorobanServer.getEvents).toHaveBeenCalledWith({\n startLedger: 50,\n filters: [{ type: 'contract', contractIds: ['C_TEST_CONTRACT_ID'] }],\n limit: 100,\n });\n expect(prismaMock.indexerCursor.upsert).toHaveBeenCalledWith({\n where: { contractId: 'C_TEST_CONTRACT_ID' },\n update: { lastLedger: 56 },\n create: { contractId: 'C_TEST_CONTRACT_ID', lastLedger: 56 },\n });\n expect(getCursor()).toBe(56);\n });\n\n it('prevents raw event id from being dispatched twice via IndexerEvent replay guard', async () => {\n sorobanServer.getLatestLedger.mockResolvedValue({ sequence: 10 });\n sorobanServer.getEvents.mockResolvedValue({\n events: [\n {\n id: 'evt-duplicate',\n topic: [],\n value: null,\n ledger: 10,\n txHash: 'hash-dup',\n },\n ],\n });\n prismaMock.indexerCursor.findUnique.mockResolvedValue({\n contractId: 'C_TEST_CONTRACT_ID',\n lastLedger: 10,\n });\n prismaMock.indexerEvent.findUnique.mockResolvedValue({\n id: 'evt-duplicate',\n topic: '',\n ledger: 10,\n });\n\n const handler = jest.fn();\n registerEventHandler('', handler);\n\n await tick();\n\n expect(handler).not.toHaveBeenCalled();\n expect(prismaMock.indexerEvent.create).not.toHaveBeenCalled();\n });\n\n it('skips dispatching on topic with no registered handler without throwing', async () => {\n await expect(\n dispatch({\n id: 'test-id',\n topic: 'unregistered_topic',\n ledger: 1,\n txHash: 'hash',\n data: { foo: 'bar' },\n }),\n ).resolves.not.toThrow();\n });\n\n it('logs error on bad event and continues poll loop', async () => {\n sorobanServer.getLatestLedger.mockResolvedValue({ sequence: 20 });\n sorobanServer.getEvents.mockResolvedValue({\n events: [\n {\n id: 'evt-bad',\n topic: [],\n value: null,\n ledger: 20,\n txHash: 'hash-bad',\n },\n {\n id: 'evt-good',\n topic: [],\n value: null,\n ledger: 20,\n txHash: 'hash-good',\n },\n ],\n });\n prismaMock.indexerCursor.findUnique.mockResolvedValue({\n contractId: 'C_TEST_CONTRACT_ID',\n lastLedger: 20,\n });\n prismaMock.indexerEvent.findUnique.mockResolvedValue(null);\n\n const handler = jest\n .fn()\n .mockImplementationOnce(() => {\n throw new Error('Handler failed on bad event');\n })\n .mockImplementationOnce(() => {});\n registerEventHandler('', handler);\n\n await tick();\n\n expect(handler).toHaveBeenCalledTimes(2);\n expect(prismaMock.indexerEvent.create).toHaveBeenCalledTimes(1);\n expect(prismaMock.indexerEvent.create).toHaveBeenCalledWith({\n data: {\n id: 'evt-good',\n topic: '',\n ledger: 20,\n },\n });\n });\n});\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\invoice-pdf.services.test.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\invoice.schema.test.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\invoice.services.test.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\merchant.profile.services.test.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/no-unused-vars", - "severity": 2, - "message": "'jest' is defined but never used. Allowed unused vars must match /^_/u.", - "line": 1, - "column": 10, - "nodeType": null, - "messageId": "unusedVar", - "endLine": 1, - "endColumn": 14 - } - ], - "suppressedMessages": [], - "errorCount": 1, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import { jest } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { getMyProfile, updateMyProfile } = await import('../../src/services/merchant.services.js');\n\nconst baseMerchant = {\n id: 'uuid-1',\n merchantId: 1,\n address: '0x123',\n account: 'CCONTRACT',\n email: 'ada@example.com',\n firstName: 'Ada',\n lastName: 'Lovelace',\n businessName: 'Analytical Engines',\n category: 'software',\n description: 'We build computing machines.',\n logo: 'https://example.com/logo.png',\n webhook: null,\n active: true,\n verified: false,\n emailVerified: false,\n registered: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n // Internal relations that the sanitizer allow-list must strip from its output.\n refreshTokens: [{ id: 'rt-1', token: 'secret-token' }],\n apiKeys: [{ id: 'ak-1', keyHash: 'hashed-secret' }],\n};\n\ndescribe('getMyProfile', () => {\n beforeEach(() => mockReset(prismaMock));\n\n test('returns the sanitized profile including account and webhook', async () => {\n prismaMock.merchant.findUnique.mockResolvedValue(baseMerchant);\n\n const result = await getMyProfile('uuid-1');\n\n expect(prismaMock.merchant.findUnique).toHaveBeenCalledWith({ where: { id: 'uuid-1' } });\n expect(result).toMatchObject({ id: 'uuid-1', account: 'CCONTRACT', webhook: null });\n expect(result).not.toHaveProperty('refreshTokens');\n expect(result).not.toHaveProperty('apiKeys');\n });\n\n test('throws AppError(404) when the merchant does not exist', async () => {\n prismaMock.merchant.findUnique.mockResolvedValue(null);\n\n await expect(getMyProfile('missing')).rejects.toMatchObject({ statusCode: 404 });\n });\n});\n\ndescribe('updateMyProfile', () => {\n beforeEach(() => mockReset(prismaMock));\n\n test('writes only the editable fields present in the payload, trimmed', async () => {\n prismaMock.merchant.update.mockImplementation(async (args: any) => ({\n ...baseMerchant,\n ...args.data,\n }));\n\n await updateMyProfile('uuid-1', {\n firstName: ' Grace ',\n webhook: 'https://example.com/hook',\n });\n\n expect(prismaMock.merchant.update).toHaveBeenCalledWith({\n where: { id: 'uuid-1' },\n data: { firstName: 'Grace', webhook: 'https://example.com/hook' },\n });\n });\n\n test('normalizes a cleared logo/webhook to null', async () => {\n prismaMock.merchant.update.mockImplementation(async (args: any) => ({\n ...baseMerchant,\n ...args.data,\n }));\n\n await updateMyProfile('uuid-1', { logo: '', webhook: null });\n\n expect(prismaMock.merchant.update).toHaveBeenCalledWith({\n where: { id: 'uuid-1' },\n data: { logo: null, webhook: null },\n });\n });\n\n test('returns the sanitized updated profile', async () => {\n prismaMock.merchant.update.mockImplementation(async (args: any) => ({\n ...baseMerchant,\n ...args.data,\n }));\n\n const result = await updateMyProfile('uuid-1', { businessName: 'New Co' });\n\n expect(result).toMatchObject({ businessName: 'New Co' });\n expect(result).not.toHaveProperty('refreshTokens');\n });\n\n test('never writes merchantKey even if the caller smuggles it into the payload', async () => {\n prismaMock.merchant.update.mockImplementation(async (args: any) => ({\n ...baseMerchant,\n ...args.data,\n }));\n\n // A profile update must not be able to overwrite the signing key.\n await updateMyProfile('uuid-1', {\n businessName: 'New Co',\n merchantKey: 'attacker-controlled-key',\n } as any);\n\n const updateArgs = prismaMock.merchant.update.mock.calls[0][0];\n expect(updateArgs.data).not.toHaveProperty('merchantKey');\n expect(updateArgs).toEqual({ where: { id: 'uuid-1' }, data: { businessName: 'New Co' } });\n });\n});\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\merchant.register.test.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 13, - "column": 3, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 13, - "endColumn": 16 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 14, - "column": 3, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 14, - "endColumn": 18 - }, - { - "ruleId": "@typescript-eslint/explicit-function-return-type", - "severity": 1, - "message": "Missing return type on function.", - "line": 15, - "column": 3, - "nodeType": "ArrowFunctionExpression", - "messageId": "missingReturnType", - "endLine": 15, - "endColumn": 24 - } - ], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 3, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import { jest } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\n\nconst sendOtpMock = jest.fn(async () => undefined);\n\njest.unstable_mockModule('../../src/services/email.service.js', () => ({\n __esModule: true,\n sendOtp: sendOtpMock,\n}));\n\njest.unstable_mockModule('../../src/services/otp.services.js', () => ({\n __esModule: true,\n generateOtp: () => '123456',\n hashOtp: async () => 'hashed-otp',\n verifyOtpHash: async () => true,\n issueEmailOtp: jest.fn(),\n verifyEmailOtp: jest.fn(),\n resendEmailOtp: jest.fn(),\n}));\n\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { registerMerchant } = await import('../../src/services/merchant.services.js');\nconst { AppError } = await import('../../src/utils/errors.js');\n\nconst baseMerchant = {\n id: 'uuid-1',\n merchantId: 1,\n email: null,\n address: '0x123',\n firstName: null,\n lastName: null,\n businessName: null,\n category: null,\n description: null,\n logo: null,\n active: true,\n verified: false,\n emailVerified: false,\n registered: false,\n emailOtp: null,\n emailOtpExpiresAt: null,\n createdAt: new Date(),\n updatedAt: new Date(),\n};\n\nconst validPayload = {\n firstName: 'Ada',\n lastName: 'Lovelace',\n email: 'Ada@Example.com',\n businessName: 'Analytical Engines',\n category: 'software',\n description: 'We build computing machines.',\n};\n\ndescribe('registerMerchant service', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n sendOtpMock.mockClear();\n });\n\n test('completes registration, stores OTP hash and sends email', async () => {\n prismaMock.merchant.findUnique.mockResolvedValue(baseMerchant as any);\n prismaMock.merchant.findFirst.mockResolvedValue(null);\n prismaMock.merchant.update.mockImplementation(async (args: any) => ({\n ...baseMerchant,\n ...args.data,\n }));\n\n const result = await registerMerchant('uuid-1', validPayload);\n\n expect(prismaMock.merchant.update).toHaveBeenCalledWith({\n where: { id: 'uuid-1' },\n data: expect.objectContaining({\n firstName: 'Ada',\n lastName: 'Lovelace',\n email: 'ada@example.com',\n businessName: 'Analytical Engines',\n category: 'software',\n description: 'We build computing machines.',\n logo: null,\n emailVerified: false,\n registered: true,\n emailOtp: 'hashed-otp',\n emailOtpExpiresAt: expect.any(Date),\n }),\n });\n expect(sendOtpMock).toHaveBeenCalledWith('ada@example.com', '123456', 'Ada');\n expect(result.emailVerified).toBe(false);\n expect(result.registered).toBe(true);\n });\n\n test('throws 404 when merchant does not exist', async () => {\n prismaMock.merchant.findUnique.mockResolvedValue(null);\n\n await expect(registerMerchant('missing', validPayload)).rejects.toMatchObject({\n statusCode: 404,\n });\n expect(sendOtpMock).not.toHaveBeenCalled();\n });\n\n test('throws 409 when profile already set up', async () => {\n prismaMock.merchant.findUnique.mockResolvedValue({\n ...baseMerchant,\n registered: true,\n } as any);\n\n await expect(registerMerchant('uuid-1', validPayload)).rejects.toMatchObject({\n statusCode: 409,\n message: 'Profile already set up',\n });\n expect(prismaMock.merchant.update).not.toHaveBeenCalled();\n });\n\n test('throws 409 when email already registered by another merchant', async () => {\n prismaMock.merchant.findUnique.mockResolvedValue(baseMerchant as any);\n prismaMock.merchant.findFirst.mockResolvedValue({\n ...baseMerchant,\n id: 'uuid-2',\n email: 'ada@example.com',\n } as any);\n\n await expect(registerMerchant('uuid-1', validPayload)).rejects.toMatchObject({\n statusCode: 409,\n message: 'Email already registered',\n });\n expect(prismaMock.merchant.update).not.toHaveBeenCalled();\n });\n\n test('AppError carries the provided status code', () => {\n const err = new AppError(409, 'Email already registered');\n expect(err.statusCode).toBe(409);\n expect(err.message).toBe('Email already registered');\n });\n});\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\merchant.services.test.ts", - "messages": [ - { - "ruleId": "@typescript-eslint/no-unused-vars", - "severity": 2, - "message": "'jest' is defined but never used. Allowed unused vars must match /^_/u.", - "line": 1, - "column": 10, - "nodeType": null, - "messageId": "unusedVar", - "endLine": 1, - "endColumn": 14 - } - ], - "suppressedMessages": [], - "errorCount": 1, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "source": "import { jest } from '@jest/globals';\nimport { mockReset } from 'jest-mock-extended';\n\n// Wait for the mock to be applied\nconst { default: prismaMock } = (await import('../../src/config/prisma.js')) as any;\nconst { createMerchant, getMerchant, listMerchants } = await import(\n '../../src/services/merchant.services.js'\n);\n\ndescribe('Merchant Services', () => {\n beforeEach(() => {\n mockReset(prismaMock);\n });\n\n test('should create a new merchant', async () => {\n const merchantData = {\n merchantId: 1,\n address: '0x123',\n email: 'test@example.com',\n };\n\n const expectedMerchant = {\n id: 'uuid-1',\n ...merchantData,\n active: true,\n verified: false,\n createdAt: new Date(),\n updatedAt: new Date(),\n };\n\n prismaMock.merchant.create.mockResolvedValue(expectedMerchant);\n\n const result = await createMerchant(merchantData);\n\n expect(result).toEqual(expectedMerchant);\n expect(prismaMock.merchant.create).toHaveBeenCalledWith({\n data: merchantData,\n });\n });\n\n test('should get a merchant by merchantId', async () => {\n const expectedMerchant = {\n id: 'uuid-1',\n merchantId: 1,\n address: '0x123',\n email: 'test@example.com',\n active: true,\n verified: false,\n createdAt: new Date(),\n updatedAt: new Date(),\n };\n\n prismaMock.merchant.findUnique.mockResolvedValue(expectedMerchant);\n\n const result = await getMerchant(1);\n\n expect(result).toEqual(expectedMerchant);\n expect(prismaMock.merchant.findUnique).toHaveBeenCalledWith({\n where: { merchantId: 1 },\n });\n });\n\n test('should list merchants', async () => {\n const merchants = [\n {\n id: 'uuid-1',\n merchantId: 1,\n address: '0x1',\n email: '1@test.com',\n active: true,\n verified: false,\n createdAt: new Date(),\n updatedAt: new Date(),\n },\n {\n id: 'uuid-2',\n merchantId: 2,\n address: '0x2',\n email: '2@test.com',\n active: true,\n verified: false,\n createdAt: new Date(),\n updatedAt: new Date(),\n },\n ];\n\n prismaMock.merchant.findMany.mockResolvedValue(merchants);\n\n const result = await listMerchants(10, 0);\n\n expect(result).toEqual(merchants);\n expect(prismaMock.merchant.findMany).toHaveBeenCalledWith({\n take: 10,\n skip: 0,\n });\n });\n});\n", - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\merchant.signing-key.services.test.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\merchant.update.validation.test.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\otp.services.test.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - }, - { - "filePath": "C:\\projetcs\\shade-backend-zeus\\tests\\unit\\subscription.schema.test.ts", - "messages": [], - "suppressedMessages": [], - "errorCount": 0, - "fatalErrorCount": 0, - "warningCount": 0, - "fixableErrorCount": 0, - "fixableWarningCount": 0, - "usedDeprecatedRules": [] - } -]