From b09975228f704b5762dba85e4776019a9e911dc2 Mon Sep 17 00:00:00 2001 From: EmmyKay0026 Date: Mon, 29 Jun 2026 11:49:00 +0100 Subject: [PATCH] feat: Swagger addition/OpenAPI documentation for all endpoints --- .github/workflows/backend-ci.yml | 3 + apps/backend/pnpm-lock.yaml | 1485 ++++++++++------- .../backend/src/api-key/api-key.controller.ts | 24 + .../src/app-version/app-version.controller.ts | 6 +- apps/backend/src/app.controller.ts | 6 +- apps/backend/src/main.ts | 29 +- .../src/modules/admin/admin.controller.ts | 44 + .../admin-escrow-consistency.controller.ts | 17 +- .../modules/assets/admin-assets.controller.ts | 41 + .../src/modules/assets/assets.controller.ts | 18 +- .../src/modules/assets/dto/asset.dto.ts | 13 + .../auth/controllers/auth.controller.ts | 85 + apps/backend/src/modules/auth/dto/auth.dto.ts | 12 + .../src/modules/auth/dto/profile.dto.ts | 6 + .../escrow-scheduler.controller.ts | 23 + .../escrow/controllers/escrow.controller.ts | 115 +- .../escrow/controllers/events.controller.ts | 8 +- .../modules/escrow/dto/cancel-escrow.dto.ts | 2 + .../modules/escrow/dto/create-escrow.dto.ts | 17 + .../src/modules/escrow/dto/dispute.dto.ts | 7 + .../escrow/dto/fulfill-condition.dto.ts | 3 + .../src/modules/escrow/dto/fund-escrow.dto.ts | 2 + .../modules/escrow/dto/list-escrows.dto.ts | 11 + .../src/modules/escrow/dto/list-events.dto.ts | 10 + .../modules/escrow/dto/update-escrow.dto.ts | 4 + .../src/modules/health/health.controller.ts | 18 +- .../src/modules/ipfs/ipfs.controller.ts | 7 +- .../controllers/stellar-event.controller.ts | 14 + .../src/modules/webhook/webhook.controller.ts | 24 + .../entities/update-preferences.dto.ts | 4 + .../notifications/notifications.controller.ts | 29 + apps/backend/src/swagger-docs.spec.ts | 43 + 32 files changed, 1497 insertions(+), 633 deletions(-) create mode 100644 apps/backend/src/swagger-docs.spec.ts diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index 313c18ee..9943b18f 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -39,5 +39,8 @@ jobs: - name: Build run: npm run build + - name: Swagger coverage check + run: npm test -- --runInBand src/swagger-docs.spec.ts + - name: Test run: npm run test diff --git a/apps/backend/pnpm-lock.yaml b/apps/backend/pnpm-lock.yaml index 8db5d74d..99ca788f 100644 --- a/apps/backend/pnpm-lock.yaml +++ b/apps/backend/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: '@nestjs/swagger': specifier: ^11.2.0 version: 11.2.6(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) + '@nestjs/terminus': + specifier: ^11.0.0 + version: 11.1.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/typeorm@11.0.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3))))(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3))) '@nestjs/throttler': specifier: ^6.5.0 version: 6.5.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2) @@ -53,6 +56,9 @@ importers: '@types/bcrypt': specifier: ^6.0.0 version: 6.0.0 + '@types/multer': + specifier: ^1.4.12 + version: 1.4.13 '@types/passport-jwt': specifier: ^4.0.1 version: 4.0.1 @@ -71,6 +77,9 @@ importers: dotenv: specifier: ^17.3.1 version: 17.3.1 + multer: + specifier: ^1.4.5-lts.1 + version: 1.4.5-lts.2 nodemailer: specifier: ^8.0.3 version: 8.0.4 @@ -121,8 +130,8 @@ importers: specifier: ^5.0.0 version: 5.0.6 '@types/jest': - specifier: ^30.0.0 - version: 30.0.0 + specifier: ^29.5.14 + version: 29.5.14 '@types/node': specifier: ^22.19.11 version: 22.19.15 @@ -145,8 +154,8 @@ importers: specifier: ^16.0.0 version: 16.5.0 jest: - specifier: ^30.0.0 - version: 30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) prettier: specifier: ^3.4.2 version: 3.8.1 @@ -157,8 +166,8 @@ importers: specifier: ^7.0.0 version: 7.2.2 ts-jest: - specifier: ^29.2.5 - version: 29.4.6(@babel/core@7.29.0)(@jest/transform@30.3.0)(@jest/types@30.3.0)(babel-jest@30.3.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)))(typescript@5.9.3) + specifier: ^29.4.11 + version: 29.4.11(@babel/core@7.29.0)(@jest/transform@30.3.0)(@jest/types@30.3.0)(babel-jest@30.3.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)))(typescript@5.9.3) ts-loader: specifier: ^9.5.2 version: 9.5.4(typescript@5.9.3)(webpack@5.104.1) @@ -384,15 +393,6 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - '@emnapi/core@1.9.1': - resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} - - '@emnapi/runtime@1.9.1': - resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} - - '@emnapi/wasi-threads@1.2.0': - resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} - '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -605,84 +605,84 @@ packages: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} - '@jest/console@30.3.0': - resolution: {integrity: sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/console@29.7.0': + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jest/core@30.3.0': - resolution: {integrity: sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/core@29.7.0': + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: node-notifier: optional: true - '@jest/diff-sequences@30.3.0': - resolution: {integrity: sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/environment@30.3.0': - resolution: {integrity: sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - - '@jest/expect-utils@30.3.0': - resolution: {integrity: sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/environment@29.7.0': + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jest/expect@30.3.0': - resolution: {integrity: sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/expect-utils@29.7.0': + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jest/fake-timers@30.3.0': - resolution: {integrity: sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/expect@29.7.0': + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.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/fake-timers@29.7.0': + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jest/globals@30.3.0': - resolution: {integrity: sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/globals@29.7.0': + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} '@jest/pattern@30.0.1': resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/reporters@30.3.0': - resolution: {integrity: sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/reporters@29.7.0': + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: node-notifier: optional: true + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/schemas@30.0.5': resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/snapshot-utils@30.3.0': - resolution: {integrity: sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/source-map@29.6.3': + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.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@29.7.0': + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jest/test-result@30.3.0': - resolution: {integrity: sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/test-sequencer@29.7.0': + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jest/test-sequencer@30.3.0': - resolution: {integrity: sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/transform@29.7.0': + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} '@jest/transform@30.3.0': resolution: {integrity: sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/types@30.3.0': resolution: {integrity: sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -716,9 +716,6 @@ packages: '@microsoft/tsdoc@0.16.0': resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} - '@napi-rs/wasm-runtime@0.2.12': - resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@nestjs/cli@11.0.16': resolution: {integrity: sha512-P0H+Vcjki6P5160E5QnMt3Q0X5FTg4PZkP99Ig4lm/4JWqfw32j3EXv3YBTJ2DmxLwOQ/IS9F7dzKpMAgzKTGg==} engines: {node: '>= 20.11'} @@ -834,6 +831,54 @@ packages: class-validator: optional: true + '@nestjs/terminus@11.1.1': + resolution: {integrity: sha512-Ssql79H+EQY/Wg108eJqN4NiNsO/tLrj+qbzOWSQUf2JE4vJQ2RG3WTqUOrYjfjWmVHD3+Ys0+azed7LSMKScw==} + peerDependencies: + '@grpc/grpc-js': '*' + '@grpc/proto-loader': '*' + '@mikro-orm/core': '*' + '@mikro-orm/nestjs': '*' + '@nestjs/axios': ^2.0.0 || ^3.0.0 || ^4.0.0 + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/core': ^10.0.0 || ^11.0.0 + '@nestjs/microservices': ^10.0.0 || ^11.0.0 + '@nestjs/mongoose': ^11.0.0 + '@nestjs/sequelize': ^10.0.0 || ^11.0.0 + '@nestjs/typeorm': ^10.0.0 || ^11.0.0 + '@prisma/client': '*' + mongoose: '*' + reflect-metadata: 0.1.x || 0.2.x + rxjs: 7.x + sequelize: '*' + typeorm: '*' + peerDependenciesMeta: + '@grpc/grpc-js': + optional: true + '@grpc/proto-loader': + optional: true + '@mikro-orm/core': + optional: true + '@mikro-orm/nestjs': + optional: true + '@nestjs/axios': + optional: true + '@nestjs/microservices': + optional: true + '@nestjs/mongoose': + optional: true + '@nestjs/sequelize': + optional: true + '@nestjs/typeorm': + optional: true + '@prisma/client': + optional: true + mongoose: + optional: true + sequelize: + optional: true + typeorm: + optional: true + '@nestjs/testing@11.1.17': resolution: {integrity: sha512-lNffw+z+2USewmw4W0tsK+Rq94A2N4PiHbcqoRUu5y8fnqxQeIWGHhjo5BFCqj7eivqJBhT7WdRydxVq4rAHzg==} peerDependencies: @@ -910,14 +955,17 @@ packages: '@scarf/scarf@1.4.0': resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + '@sinclair/typebox@0.27.10': + resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} + '@sinclair/typebox@0.34.48': resolution: {integrity: sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==} '@sinonjs/commons@3.0.1': resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} - '@sinonjs/fake-timers@15.1.1': - resolution: {integrity: sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==} + '@sinonjs/fake-timers@10.3.0': + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} @@ -964,9 +1012,6 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1009,6 +1054,9 @@ packages: '@types/express@5.0.6': resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + '@types/graceful-fs@4.1.9': + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} @@ -1021,8 +1069,8 @@ packages: '@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/jest@29.5.14': + resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1039,6 +1087,9 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/multer@1.4.13': + resolution: {integrity: sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==} + '@types/node@22.19.15': resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} @@ -1148,109 +1199,7 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - - '@unrs/resolver-binding-android-arm-eabi@1.11.1': - resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} - cpu: [arm] - os: [android] - - '@unrs/resolver-binding-android-arm64@1.11.1': - resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} - cpu: [arm64] - os: [android] - - '@unrs/resolver-binding-darwin-arm64@1.11.1': - resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} - cpu: [arm64] - os: [darwin] - - '@unrs/resolver-binding-darwin-x64@1.11.1': - resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} - cpu: [x64] - os: [darwin] - - '@unrs/resolver-binding-freebsd-x64@1.11.1': - resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} - cpu: [x64] - os: [freebsd] - - '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': - resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} - cpu: [arm] - os: [linux] - - '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': - resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} - cpu: [arm] - os: [linux] - - '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': - resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@unrs/resolver-binding-linux-arm64-musl@1.11.1': - resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': - resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': - resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': - resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': - resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@unrs/resolver-binding-linux-x64-gnu@1.11.1': - resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@unrs/resolver-binding-linux-x64-musl@1.11.1': - resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@unrs/resolver-binding-wasm32-wasi@1.11.1': - resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - - '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': - resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} - cpu: [arm64] - os: [win32] - - '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': - resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} - cpu: [ia32] - os: [win32] - - '@unrs/resolver-binding-win32-x64-msvc@1.11.1': - resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} - cpu: [x64] - os: [win32] + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -1381,6 +1330,9 @@ packages: ajv@8.18.0: resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -1457,16 +1409,30 @@ packages: axios@1.13.6: resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} + babel-jest@29.7.0: + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + babel-jest@30.3.0: resolution: {integrity: sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==} 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@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + babel-plugin-istanbul@7.0.1: resolution: {integrity: sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==} engines: {node: '>=12'} + babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + babel-plugin-jest-hoist@30.3.0: resolution: {integrity: sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -1476,6 +1442,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0 || ^8.0.0-0 + babel-preset-jest@29.6.3: + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + babel-preset-jest@30.3.0: resolution: {integrity: sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -1541,6 +1513,10 @@ packages: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} + boxen@5.1.2: + resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} + engines: {node: '>=10'} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -1629,6 +1605,10 @@ packages: chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + check-disk-space@3.4.0: + resolution: {integrity: sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==} + engines: {node: '>=16'} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -1644,12 +1624,16 @@ packages: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + 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==} + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} class-transformer@0.5.1: resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} @@ -1661,6 +1645,10 @@ packages: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} + cli-boxes@2.2.1: + resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} + engines: {node: '>=6'} + cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -1728,6 +1716,10 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + concat-stream@1.6.2: + resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} + engines: {'0': node >= 0.8} + concat-stream@2.0.0: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} @@ -1777,6 +1769,11 @@ packages: typescript: optional: true + create-jest@29.7.0: + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -1852,6 +1849,10 @@ packages: dezalgo@1.0.4: resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + diff@4.0.4: resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} engines: {node: '>=0.3.1'} @@ -2059,17 +2060,17 @@ packages: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} - exit-x@0.2.2: - resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} + exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} - expect@30.3.0: - resolution: {integrity: sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} express@5.2.1: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} @@ -2274,8 +2275,8 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - handlebars@4.7.8: - resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + handlebars@4.7.9: + resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} engines: {node: '>=0.4.7'} hasBin: true @@ -2301,6 +2302,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -2390,6 +2395,10 @@ packages: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2436,6 +2445,9 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -2446,6 +2458,10 @@ packages: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} + istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + istanbul-lib-instrument@6.0.3: resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} engines: {node: '>=10'} @@ -2454,8 +2470,8 @@ packages: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} engines: {node: '>=10'} - istanbul-lib-source-maps@5.0.6: - resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} istanbul-reports@3.2.0: @@ -2469,17 +2485,17 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jest-changed-files@30.3.0: - resolution: {integrity: sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-circus@30.3.0: - resolution: {integrity: sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-cli@30.3.0: - resolution: {integrity: sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-cli@29.7.0: + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -2487,56 +2503,61 @@ packages: node-notifier: optional: true - jest-config@30.3.0: - resolution: {integrity: sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-config@29.7.0: + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.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.3.0: - resolution: {integrity: sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-docblock@30.2.0: - resolution: {integrity: sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-each@30.3.0: - resolution: {integrity: sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-environment-node@30.3.0: - resolution: {integrity: sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} jest-haste-map@30.3.0: resolution: {integrity: sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-leak-detector@30.3.0: - resolution: {integrity: sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-matcher-utils@30.3.0: - resolution: {integrity: sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-message-util@30.3.0: - resolution: {integrity: sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-mock@30.3.0: - resolution: {integrity: sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} jest-pnp-resolver@1.2.3: resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} @@ -2547,53 +2568,65 @@ packages: jest-resolve: optional: true + jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-regex-util@30.0.1: resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-resolve-dependencies@30.3.0: - resolution: {integrity: sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-resolve@30.3.0: - resolution: {integrity: sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-runner@30.3.0: - resolution: {integrity: sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-runtime@30.3.0: - resolution: {integrity: sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-snapshot@30.3.0: - resolution: {integrity: sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} jest-util@30.3.0: resolution: {integrity: sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-validate@30.3.0: - resolution: {integrity: sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-watcher@30.3.0: - resolution: {integrity: sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} jest-worker@27.5.1: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jest-worker@30.3.0: resolution: {integrity: sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest@30.3.0: - resolution: {integrity: sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + jest@29.7.0: + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 @@ -2656,6 +2689,10 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -2868,6 +2905,10 @@ packages: mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} @@ -2876,6 +2917,11 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + multer@1.4.5-lts.2: + resolution: {integrity: sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==} + engines: {node: '>= 6.0.0'} + deprecated: Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version. + multer@2.1.1: resolution: {integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==} engines: {node: '>= 10.16.0'} @@ -2887,11 +2933,6 @@ packages: napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} - 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==} @@ -3057,6 +3098,9 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} @@ -3125,9 +3169,12 @@ packages: engines: {node: '>=14'} hasBin: true - pretty-format@30.3.0: - resolution: {integrity: sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} promise-inflight@1.0.1: resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} @@ -3141,6 +3188,10 @@ packages: resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} engines: {node: '>=10'} + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -3155,8 +3206,8 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - pure-rand@7.0.1: - resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} qs@6.15.0: resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} @@ -3180,6 +3231,9 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -3215,6 +3269,15 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + restore-cursor@3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} @@ -3238,6 +3301,9 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -3261,6 +3327,11 @@ packages: engines: {node: '>=10'} 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'} @@ -3321,6 +3392,9 @@ packages: simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -3412,6 +3486,9 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -3463,6 +3540,10 @@ packages: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} engines: {node: '>=10'} + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + swagger-ui-dist@5.31.0: resolution: {integrity: sha512-zSUTIck02fSga6rc0RZP3b7J7wgHXwLea8ZjgLA3Vgnb8QeOl3Wou2/j5QkzSGeoz6HusP/coYuJl33aQxQZpg==} @@ -3556,8 +3637,8 @@ packages: peerDependencies: typescript: '>=4.8.4' - ts-jest@29.4.6: - resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==} + ts-jest@29.4.11: + resolution: {integrity: sha512-IrFl7l9AuB/qrNw5quqvAv/hmKMb8dhWOH4jQOGo0Oq8tCeo1O86/iTFG1FaRimgUkF13l4PcepO8ATFT6Ns4g==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -3568,7 +3649,7 @@ packages: esbuild: '*' jest: ^29.0.0 || ^30.0.0 jest-util: ^29.0.0 || ^30.0.0 - typescript: '>=4.3 <6' + typescript: '>=4.3 <7' peerDependenciesMeta: '@babel/core': optional: true @@ -3629,6 +3710,10 @@ packages: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} @@ -3749,9 +3834,6 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - unrs-resolver@1.11.1: - resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} - update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -3830,6 +3912,10 @@ packages: wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + widest-line@3.1.0: + resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} + engines: {node: '>=8'} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -3852,6 +3938,10 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + write-file-atomic@5.0.1: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -3868,6 +3958,10 @@ packages: utf-8-validate: optional: true + 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'} @@ -4152,22 +4246,6 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@emnapi/core@1.9.1': - dependencies: - '@emnapi/wasi-threads': 1.2.0 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.9.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.0': - dependencies: - tslib: 2.8.1 - optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': dependencies: eslint: 9.39.4 @@ -4387,87 +4465,83 @@ snapshots: '@istanbuljs/schema@0.1.3': {} - '@jest/console@30.3.0': + '@jest/console@29.7.0': dependencies: - '@jest/types': 30.3.0 + '@jest/types': 29.6.3 '@types/node': 22.19.15 chalk: 4.1.2 - jest-message-util: 30.3.0 - jest-util: 30.3.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@30.3.0(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3))': + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3))': dependencies: - '@jest/console': 30.3.0 - '@jest/pattern': 30.0.1 - '@jest/reporters': 30.3.0 - '@jest/test-result': 30.3.0 - '@jest/transform': 30.3.0 - '@jest/types': 30.3.0 + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 22.19.15 ansi-escapes: 4.3.2 chalk: 4.1.2 - ci-info: 4.4.0 - exit-x: 0.2.2 + ci-info: 3.9.0 + exit: 0.1.2 graceful-fs: 4.2.11 - jest-changed-files: 30.3.0 - jest-config: 30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) - jest-haste-map: 30.3.0 - jest-message-util: 30.3.0 - jest-regex-util: 30.0.1 - jest-resolve: 30.3.0 - jest-resolve-dependencies: 30.3.0 - jest-runner: 30.3.0 - jest-runtime: 30.3.0 - jest-snapshot: 30.3.0 - jest-util: 30.3.0 - jest-validate: 30.3.0 - jest-watcher: 30.3.0 - pretty-format: 30.3.0 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 slash: 3.0.0 + strip-ansi: 6.0.1 transitivePeerDependencies: - babel-plugin-macros - - esbuild-register - supports-color - ts-node - '@jest/diff-sequences@30.3.0': {} - - '@jest/environment@30.3.0': + '@jest/environment@29.7.0': dependencies: - '@jest/fake-timers': 30.3.0 - '@jest/types': 30.3.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 22.19.15 - jest-mock: 30.3.0 + jest-mock: 29.7.0 - '@jest/expect-utils@30.3.0': + '@jest/expect-utils@29.7.0': dependencies: - '@jest/get-type': 30.1.0 + jest-get-type: 29.6.3 - '@jest/expect@30.3.0': + '@jest/expect@29.7.0': dependencies: - expect: 30.3.0 - jest-snapshot: 30.3.0 + expect: 29.7.0 + jest-snapshot: 29.7.0 transitivePeerDependencies: - supports-color - '@jest/fake-timers@30.3.0': + '@jest/fake-timers@29.7.0': dependencies: - '@jest/types': 30.3.0 - '@sinonjs/fake-timers': 15.1.1 + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 '@types/node': 22.19.15 - jest-message-util: 30.3.0 - jest-mock: 30.3.0 - jest-util: 30.3.0 - - '@jest/get-type@30.1.0': {} + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 - '@jest/globals@30.3.0': + '@jest/globals@29.7.0': dependencies: - '@jest/environment': 30.3.0 - '@jest/expect': 30.3.0 - '@jest/types': 30.3.0 - jest-mock: 30.3.0 + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 transitivePeerDependencies: - supports-color @@ -4475,66 +4549,86 @@ snapshots: dependencies: '@types/node': 22.19.15 jest-regex-util: 30.0.1 + optional: true - '@jest/reporters@30.3.0': + '@jest/reporters@29.7.0': dependencies: '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 30.3.0 - '@jest/test-result': 30.3.0 - '@jest/transform': 30.3.0 - '@jest/types': 30.3.0 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.31 '@types/node': 22.19.15 chalk: 4.1.2 collect-v8-coverage: 1.0.3 - exit-x: 0.2.2 - glob: 10.5.0 + exit: 0.1.2 + glob: 7.2.3 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-lib-source-maps: 4.0.1 istanbul-reports: 3.2.0 - jest-message-util: 30.3.0 - jest-util: 30.3.0 - jest-worker: 30.3.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 slash: 3.0.0 string-length: 4.0.2 + strip-ansi: 6.0.1 v8-to-istanbul: 9.3.0 transitivePeerDependencies: - supports-color - '@jest/schemas@30.0.5': + '@jest/schemas@29.6.3': dependencies: - '@sinclair/typebox': 0.34.48 + '@sinclair/typebox': 0.27.10 - '@jest/snapshot-utils@30.3.0': + '@jest/schemas@30.0.5': dependencies: - '@jest/types': 30.3.0 - chalk: 4.1.2 - graceful-fs: 4.2.11 - natural-compare: 1.4.0 + '@sinclair/typebox': 0.34.48 + optional: true - '@jest/source-map@30.0.1': + '@jest/source-map@29.6.3': dependencies: '@jridgewell/trace-mapping': 0.3.31 callsites: 3.1.0 graceful-fs: 4.2.11 - '@jest/test-result@30.3.0': + '@jest/test-result@29.7.0': dependencies: - '@jest/console': 30.3.0 - '@jest/types': 30.3.0 + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 collect-v8-coverage: 1.0.3 - '@jest/test-sequencer@30.3.0': + '@jest/test-sequencer@29.7.0': dependencies: - '@jest/test-result': 30.3.0 + '@jest/test-result': 29.7.0 graceful-fs: 4.2.11 - jest-haste-map: 30.3.0 + jest-haste-map: 29.7.0 slash: 3.0.0 + '@jest/transform@29.7.0': + dependencies: + '@babel/core': 7.29.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.31 + babel-plugin-istanbul: 6.1.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: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.8 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + '@jest/transform@30.3.0': dependencies: '@babel/core': 7.29.0 @@ -4553,6 +4647,16 @@ snapshots: write-file-atomic: 5.0.1 transitivePeerDependencies: - supports-color + optional: true + + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 22.19.15 + '@types/yargs': 17.0.35 + chalk: 4.1.2 '@jest/types@30.3.0': dependencies: @@ -4563,6 +4667,7 @@ snapshots: '@types/node': 22.19.15 '@types/yargs': 17.0.35 chalk: 4.1.2 + optional: true '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -4597,13 +4702,6 @@ snapshots: '@microsoft/tsdoc@0.16.0': {} - '@napi-rs/wasm-runtime@0.2.12': - dependencies: - '@emnapi/core': 1.9.1 - '@emnapi/runtime': 1.9.1 - '@tybys/wasm-util': 0.10.1 - optional: true - '@nestjs/cli@11.0.16(@types/node@22.19.15)': dependencies: '@angular-devkit/core': 19.2.19(chokidar@4.0.3) @@ -4743,6 +4841,18 @@ snapshots: class-transformer: 0.5.1 class-validator: 0.14.4 + '@nestjs/terminus@11.1.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/typeorm@11.0.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3))))(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)))': + dependencies: + '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(@nestjs/websockets@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) + boxen: 5.1.2 + check-disk-space: 3.4.0 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + optionalDependencies: + '@nestjs/typeorm': 11.0.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3))) + typeorm: 0.3.28(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) + '@nestjs/testing@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/platform-express@11.1.17)': dependencies: '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -4810,13 +4920,16 @@ snapshots: '@scarf/scarf@1.4.0': {} - '@sinclair/typebox@0.34.48': {} + '@sinclair/typebox@0.27.10': {} + + '@sinclair/typebox@0.34.48': + optional: true '@sinonjs/commons@3.0.1': dependencies: type-detect: 4.0.8 - '@sinonjs/fake-timers@15.1.1': + '@sinonjs/fake-timers@10.3.0': dependencies: '@sinonjs/commons': 3.0.1 @@ -4882,11 +4995,6 @@ snapshots: '@tsconfig/node16@1.0.4': {} - '@tybys/wasm-util@0.10.1': - dependencies: - tslib: 2.8.1 - optional: true - '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.2 @@ -4952,6 +5060,10 @@ snapshots: '@types/express-serve-static-core': 5.1.1 '@types/serve-static': 2.2.0 + '@types/graceful-fs@4.1.9': + dependencies: + '@types/node': 22.19.15 + '@types/http-errors@2.0.5': {} '@types/istanbul-lib-coverage@2.0.6': {} @@ -4964,10 +5076,10 @@ snapshots: dependencies: '@types/istanbul-lib-report': 3.0.3 - '@types/jest@30.0.0': + '@types/jest@29.5.14': dependencies: - expect: 30.3.0 - pretty-format: 30.3.0 + expect: 29.7.0 + pretty-format: 29.7.0 '@types/json-schema@7.0.15': {} @@ -4982,6 +5094,10 @@ snapshots: '@types/ms@2.1.0': {} + '@types/multer@1.4.13': + dependencies: + '@types/express': 5.0.6 + '@types/node@22.19.15': dependencies: undici-types: 6.21.0 @@ -5134,65 +5250,7 @@ snapshots: '@typescript-eslint/types': 8.57.2 eslint-visitor-keys: 5.0.1 - '@ungap/structured-clone@1.3.0': {} - - '@unrs/resolver-binding-android-arm-eabi@1.11.1': - optional: true - - '@unrs/resolver-binding-android-arm64@1.11.1': - optional: true - - '@unrs/resolver-binding-darwin-arm64@1.11.1': - optional: true - - '@unrs/resolver-binding-darwin-x64@1.11.1': - optional: true - - '@unrs/resolver-binding-freebsd-x64@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm64-musl@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-x64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-x64-musl@1.11.1': - optional: true - - '@unrs/resolver-binding-wasm32-wasi@1.11.1': - dependencies: - '@napi-rs/wasm-runtime': 0.2.12 - optional: true - - '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': - optional: true - - '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': - optional: true - - '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + '@ungap/structured-clone@1.3.0': optional: true '@webassemblyjs/ast@1.14.1': @@ -5358,6 +5416,10 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + ansi-colors@4.1.3: {} ansi-escapes@4.3.2: @@ -5422,6 +5484,19 @@ snapshots: transitivePeerDependencies: - debug + babel-jest@29.7.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.29.0) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + babel-jest@30.3.0(@babel/core@7.29.0): dependencies: '@babel/core': 7.29.0 @@ -5434,6 +5509,17 @@ snapshots: slash: 3.0.0 transitivePeerDependencies: - supports-color + optional: true + + babel-plugin-istanbul@6.1.1: + dependencies: + '@babel/helper-plugin-utils': 7.28.6 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color babel-plugin-istanbul@7.0.1: dependencies: @@ -5444,10 +5530,19 @@ snapshots: test-exclude: 6.0.0 transitivePeerDependencies: - supports-color + optional: true + + babel-plugin-jest-hoist@29.6.3: + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.28.0 babel-plugin-jest-hoist@30.3.0: dependencies: '@types/babel__core': 7.20.5 + optional: true babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0): dependencies: @@ -5468,11 +5563,18 @@ snapshots: '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0) + babel-preset-jest@29.6.3(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + babel-preset-jest@30.3.0(@babel/core@7.29.0): dependencies: '@babel/core': 7.29.0 babel-plugin-jest-hoist: 30.3.0 babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + optional: true balanced-match@1.0.2: {} @@ -5531,6 +5633,17 @@ snapshots: transitivePeerDependencies: - supports-color + boxen@5.1.2: + dependencies: + ansi-align: 3.0.1 + camelcase: 6.3.0 + chalk: 4.1.2 + cli-boxes: 2.2.1 + string-width: 4.2.3 + type-fest: 0.20.2 + widest-line: 3.1.0 + wrap-ansi: 7.0.0 + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -5642,6 +5755,8 @@ snapshots: chardet@2.1.1: {} + check-disk-space@3.4.0: {} + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -5652,9 +5767,12 @@ snapshots: chrome-trace-event@1.0.4: {} - ci-info@4.4.0: {} + ci-info@3.9.0: {} - cjs-module-lexer@2.2.0: {} + ci-info@4.4.0: + optional: true + + cjs-module-lexer@1.4.3: {} class-transformer@0.5.1: {} @@ -5667,6 +5785,8 @@ snapshots: clean-stack@2.2.0: optional: true + cli-boxes@2.2.1: {} + cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 @@ -5722,6 +5842,13 @@ snapshots: concat-map@0.0.1: {} + concat-stream@1.6.2: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 2.3.8 + typedarray: 0.0.6 + concat-stream@2.0.0: dependencies: buffer-from: 1.1.2 @@ -5762,6 +5889,21 @@ snapshots: optionalDependencies: typescript: 5.9.3 + create-jest@29.7.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + create-require@1.1.1: {} cron@4.4.0: @@ -5819,6 +5961,8 @@ snapshots: asap: 2.0.6 wrappy: 1.0.2 + diff-sequences@29.6.3: {} + diff@4.0.4: {} dotenv-expand@12.0.3: @@ -6032,18 +6176,17 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 - exit-x@0.2.2: {} + exit@0.1.2: {} expand-template@2.0.3: {} - expect@30.3.0: + expect@29.7.0: dependencies: - '@jest/expect-utils': 30.3.0 - '@jest/get-type': 30.1.0 - jest-matcher-utils: 30.3.0 - jest-message-util: 30.3.0 - jest-mock: 30.3.0 - jest-util: 30.3.0 + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 express@5.2.1: dependencies: @@ -6294,7 +6437,7 @@ snapshots: graceful-fs@4.2.11: {} - handlebars@4.7.8: + handlebars@4.7.9: dependencies: minimist: 1.2.8 neo-async: 2.6.2 @@ -6322,6 +6465,10 @@ snapshots: dependencies: function-bind: 1.1.2 + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + html-escaper@2.0.2: {} http-cache-semantics@4.2.0: @@ -6410,6 +6557,10 @@ snapshots: is-callable@1.2.7: {} + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -6439,12 +6590,24 @@ snapshots: is-unicode-supported@0.1.0: {} + isarray@1.0.0: {} + isarray@2.0.5: {} isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} + istanbul-lib-instrument@5.2.1: + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.2 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + istanbul-lib-instrument@6.0.3: dependencies: '@babel/core': 7.29.0 @@ -6461,11 +6624,11 @@ snapshots: make-dir: 4.0.0 supports-color: 7.2.0 - istanbul-lib-source-maps@5.0.6: + istanbul-lib-source-maps@4.0.1: dependencies: - '@jridgewell/trace-mapping': 0.3.31 debug: 4.4.3 istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 transitivePeerDependencies: - supports-color @@ -6482,80 +6645,79 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jest-changed-files@30.3.0: + jest-changed-files@29.7.0: dependencies: execa: 5.1.1 - jest-util: 30.3.0 + jest-util: 29.7.0 p-limit: 3.1.0 - jest-circus@30.3.0: + jest-circus@29.7.0: dependencies: - '@jest/environment': 30.3.0 - '@jest/expect': 30.3.0 - '@jest/test-result': 30.3.0 - '@jest/types': 30.3.0 + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 22.19.15 chalk: 4.1.2 co: 4.6.0 dedent: 1.7.2 is-generator-fn: 2.1.0 - jest-each: 30.3.0 - jest-matcher-utils: 30.3.0 - jest-message-util: 30.3.0 - jest-runtime: 30.3.0 - jest-snapshot: 30.3.0 - jest-util: 30.3.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 p-limit: 3.1.0 - pretty-format: 30.3.0 - pure-rand: 7.0.1 + pretty-format: 29.7.0 + pure-rand: 6.1.0 slash: 3.0.0 stack-utils: 2.0.6 transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-cli@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)): + jest-cli@29.7.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)): dependencies: - '@jest/core': 30.3.0(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) - '@jest/test-result': 30.3.0 - '@jest/types': 30.3.0 + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 chalk: 4.1.2 - exit-x: 0.2.2 + create-jest: 29.7.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) + exit: 0.1.2 import-local: 3.2.0 - jest-config: 30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) - jest-util: 30.3.0 - jest-validate: 30.3.0 + jest-config: 29.7.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) + jest-util: 29.7.0 + jest-validate: 29.7.0 yargs: 17.7.2 transitivePeerDependencies: - '@types/node' - babel-plugin-macros - - esbuild-register - supports-color - ts-node - jest-config@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)): + jest-config@29.7.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)): dependencies: '@babel/core': 7.29.0 - '@jest/get-type': 30.1.0 - '@jest/pattern': 30.0.1 - '@jest/test-sequencer': 30.3.0 - '@jest/types': 30.3.0 - babel-jest: 30.3.0(@babel/core@7.29.0) + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.29.0) chalk: 4.1.2 - ci-info: 4.4.0 + ci-info: 3.9.0 deepmerge: 4.3.1 - glob: 10.5.0 + glob: 7.2.3 graceful-fs: 4.2.11 - jest-circus: 30.3.0 - jest-docblock: 30.2.0 - jest-environment-node: 30.3.0 - jest-regex-util: 30.0.1 - jest-resolve: 30.3.0 - jest-runner: 30.3.0 - jest-util: 30.3.0 - jest-validate: 30.3.0 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 parse-json: 5.2.0 - pretty-format: 30.3.0 + pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: @@ -6565,34 +6727,51 @@ snapshots: - babel-plugin-macros - supports-color - jest-diff@30.3.0: + jest-diff@29.7.0: dependencies: - '@jest/diff-sequences': 30.3.0 - '@jest/get-type': 30.1.0 chalk: 4.1.2 - pretty-format: 30.3.0 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 - jest-docblock@30.2.0: + jest-docblock@29.7.0: dependencies: detect-newline: 3.1.0 - jest-each@30.3.0: + jest-each@29.7.0: dependencies: - '@jest/get-type': 30.1.0 - '@jest/types': 30.3.0 + '@jest/types': 29.6.3 chalk: 4.1.2 - jest-util: 30.3.0 - pretty-format: 30.3.0 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 - jest-environment-node@30.3.0: + jest-environment-node@29.7.0: dependencies: - '@jest/environment': 30.3.0 - '@jest/fake-timers': 30.3.0 - '@jest/types': 30.3.0 + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 22.19.15 - jest-mock: 30.3.0 - jest-util: 30.3.0 - jest-validate: 30.3.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + jest-get-type@29.6.3: {} + + jest-haste-map@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 22.19.15 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 jest-haste-map@30.3.0: dependencies: @@ -6608,141 +6787,153 @@ snapshots: walker: 1.0.8 optionalDependencies: fsevents: 2.3.3 + optional: true - jest-leak-detector@30.3.0: + jest-leak-detector@29.7.0: dependencies: - '@jest/get-type': 30.1.0 - pretty-format: 30.3.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 - jest-matcher-utils@30.3.0: + jest-matcher-utils@29.7.0: dependencies: - '@jest/get-type': 30.1.0 chalk: 4.1.2 - jest-diff: 30.3.0 - pretty-format: 30.3.0 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 - jest-message-util@30.3.0: + jest-message-util@29.7.0: dependencies: '@babel/code-frame': 7.29.0 - '@jest/types': 30.3.0 + '@jest/types': 29.6.3 '@types/stack-utils': 2.0.3 chalk: 4.1.2 graceful-fs: 4.2.11 - picomatch: 4.0.4 - pretty-format: 30.3.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 slash: 3.0.0 stack-utils: 2.0.6 - jest-mock@30.3.0: + jest-mock@29.7.0: dependencies: - '@jest/types': 30.3.0 + '@jest/types': 29.6.3 '@types/node': 22.19.15 - jest-util: 30.3.0 + jest-util: 29.7.0 - jest-pnp-resolver@1.2.3(jest-resolve@30.3.0): + jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): optionalDependencies: - jest-resolve: 30.3.0 + jest-resolve: 29.7.0 + + jest-regex-util@29.6.3: {} - jest-regex-util@30.0.1: {} + jest-regex-util@30.0.1: + optional: true - jest-resolve-dependencies@30.3.0: + jest-resolve-dependencies@29.7.0: dependencies: - jest-regex-util: 30.0.1 - jest-snapshot: 30.3.0 + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 transitivePeerDependencies: - supports-color - jest-resolve@30.3.0: + jest-resolve@29.7.0: dependencies: chalk: 4.1.2 graceful-fs: 4.2.11 - jest-haste-map: 30.3.0 - jest-pnp-resolver: 1.2.3(jest-resolve@30.3.0) - jest-util: 30.3.0 - jest-validate: 30.3.0 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.12 + resolve.exports: 2.0.3 slash: 3.0.0 - unrs-resolver: 1.11.1 - jest-runner@30.3.0: + jest-runner@29.7.0: dependencies: - '@jest/console': 30.3.0 - '@jest/environment': 30.3.0 - '@jest/test-result': 30.3.0 - '@jest/transform': 30.3.0 - '@jest/types': 30.3.0 + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 22.19.15 chalk: 4.1.2 emittery: 0.13.1 - exit-x: 0.2.2 graceful-fs: 4.2.11 - jest-docblock: 30.2.0 - jest-environment-node: 30.3.0 - jest-haste-map: 30.3.0 - jest-leak-detector: 30.3.0 - jest-message-util: 30.3.0 - jest-resolve: 30.3.0 - jest-runtime: 30.3.0 - jest-util: 30.3.0 - jest-watcher: 30.3.0 - jest-worker: 30.3.0 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 p-limit: 3.1.0 source-map-support: 0.5.13 transitivePeerDependencies: - supports-color - jest-runtime@30.3.0: + jest-runtime@29.7.0: dependencies: - '@jest/environment': 30.3.0 - '@jest/fake-timers': 30.3.0 - '@jest/globals': 30.3.0 - '@jest/source-map': 30.0.1 - '@jest/test-result': 30.3.0 - '@jest/transform': 30.3.0 - '@jest/types': 30.3.0 + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 22.19.15 chalk: 4.1.2 - cjs-module-lexer: 2.2.0 + cjs-module-lexer: 1.4.3 collect-v8-coverage: 1.0.3 - glob: 10.5.0 + glob: 7.2.3 graceful-fs: 4.2.11 - jest-haste-map: 30.3.0 - jest-message-util: 30.3.0 - jest-mock: 30.3.0 - jest-regex-util: 30.0.1 - jest-resolve: 30.3.0 - jest-snapshot: 30.3.0 - jest-util: 30.3.0 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 slash: 3.0.0 strip-bom: 4.0.0 transitivePeerDependencies: - supports-color - jest-snapshot@30.3.0: + jest-snapshot@29.7.0: dependencies: '@babel/core': 7.29.0 '@babel/generator': 7.29.1 '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) '@babel/types': 7.29.0 - '@jest/expect-utils': 30.3.0 - '@jest/get-type': 30.1.0 - '@jest/snapshot-utils': 30.3.0 - '@jest/transform': 30.3.0 - '@jest/types': 30.3.0 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) chalk: 4.1.2 - expect: 30.3.0 + expect: 29.7.0 graceful-fs: 4.2.11 - jest-diff: 30.3.0 - jest-matcher-utils: 30.3.0 - jest-message-util: 30.3.0 - jest-util: 30.3.0 - pretty-format: 30.3.0 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 semver: 7.7.4 - synckit: 0.11.12 transitivePeerDependencies: - supports-color + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.19.15 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.2 + jest-util@30.3.0: dependencies: '@jest/types': 30.3.0 @@ -6751,25 +6942,26 @@ snapshots: ci-info: 4.4.0 graceful-fs: 4.2.11 picomatch: 4.0.4 + optional: true - jest-validate@30.3.0: + jest-validate@29.7.0: dependencies: - '@jest/get-type': 30.1.0 - '@jest/types': 30.3.0 + '@jest/types': 29.6.3 camelcase: 6.3.0 chalk: 4.1.2 + jest-get-type: 29.6.3 leven: 3.1.0 - pretty-format: 30.3.0 + pretty-format: 29.7.0 - jest-watcher@30.3.0: + jest-watcher@29.7.0: dependencies: - '@jest/test-result': 30.3.0 - '@jest/types': 30.3.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 22.19.15 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 - jest-util: 30.3.0 + jest-util: 29.7.0 string-length: 4.0.2 jest-worker@27.5.1: @@ -6778,6 +6970,13 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 + jest-worker@29.7.0: + dependencies: + '@types/node': 22.19.15 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + jest-worker@30.3.0: dependencies: '@types/node': 22.19.15 @@ -6785,17 +6984,17 @@ snapshots: jest-util: 30.3.0 merge-stream: 2.0.0 supports-color: 8.1.1 + optional: true - jest@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)): + jest@29.7.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)): dependencies: - '@jest/core': 30.3.0(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) - '@jest/types': 30.3.0 + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) + '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) + jest-cli: 29.7.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros - - esbuild-register - supports-color - ts-node @@ -6860,6 +7059,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + kleur@3.0.3: {} + leven@3.1.0: {} levn@0.4.1: @@ -7057,10 +7258,24 @@ snapshots: mkdirp-classic@0.5.3: {} + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + mkdirp@1.0.4: {} ms@2.1.3: {} + multer@1.4.5-lts.2: + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 1.6.2 + mkdirp: 0.5.6 + object-assign: 4.1.1 + type-is: 1.6.18 + xtend: 4.0.2 + multer@2.1.1: dependencies: append-field: 1.0.0 @@ -7072,8 +7287,6 @@ snapshots: napi-build-utils@2.0.0: {} - napi-postinstall@0.3.4: {} - natural-compare@1.4.0: {} negotiator@0.6.3: {} @@ -7239,6 +7452,8 @@ snapshots: path-key@3.1.1: {} + path-parse@1.0.7: {} + path-scurry@1.11.1: dependencies: lru-cache: 10.4.3 @@ -7296,12 +7511,14 @@ snapshots: prettier@3.8.1: {} - pretty-format@30.3.0: + pretty-format@29.7.0: dependencies: - '@jest/schemas': 30.0.5 + '@jest/schemas': 29.6.3 ansi-styles: 5.2.0 react-is: 18.3.1 + process-nextick-args@2.0.1: {} + promise-inflight@1.0.1: optional: true @@ -7311,6 +7528,11 @@ snapshots: retry: 0.12.0 optional: true + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -7325,7 +7547,7 @@ snapshots: punycode@2.3.1: {} - pure-rand@7.0.1: {} + pure-rand@6.1.0: {} qs@6.15.0: dependencies: @@ -7353,6 +7575,16 @@ snapshots: react-is@18.3.1: {} + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -7382,6 +7614,15 @@ snapshots: resolve-from@5.0.0: {} + resolve.exports@2.0.3: {} + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + restore-cursor@3.1.0: dependencies: onetime: 5.1.2 @@ -7413,6 +7654,8 @@ snapshots: dependencies: tslib: 2.8.1 + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} safer-buffer@2.1.2: {} @@ -7434,6 +7677,8 @@ snapshots: semver@7.7.4: {} + semver@7.8.5: {} + send@1.2.1: dependencies: debug: 4.4.3 @@ -7525,6 +7770,8 @@ snapshots: once: 1.4.0 simple-concat: 1.0.1 + sisteransi@1.0.5: {} + slash@3.0.0: {} smart-buffer@4.2.0: @@ -7658,6 +7905,10 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.2.0 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -7714,6 +7965,8 @@ snapshots: dependencies: has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} + swagger-ui-dist@5.31.0: dependencies: '@scarf/scarf': 1.4.0 @@ -7811,16 +8064,16 @@ snapshots: dependencies: typescript: 5.9.3 - ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.3.0)(@jest/types@30.3.0)(babel-jest@30.3.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)))(typescript@5.9.3): + ts-jest@29.4.11(@babel/core@7.29.0)(@jest/transform@30.3.0)(@jest/types@30.3.0)(babel-jest@30.3.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@29.7.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - handlebars: 4.7.8 - jest: 30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) + handlebars: 4.7.9 + jest: 29.7.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.7.4 + semver: 7.8.5 type-fest: 4.41.0 typescript: 5.9.3 yargs-parser: 21.1.1 @@ -7886,6 +8139,8 @@ snapshots: type-detect@4.0.8: {} + type-fest@0.20.2: {} + type-fest@0.21.3: {} type-fest@4.41.0: {} @@ -7971,30 +8226,6 @@ snapshots: unpipe@1.0.0: {} - unrs-resolver@1.11.1: - dependencies: - napi-postinstall: 0.3.4 - optionalDependencies: - '@unrs/resolver-binding-android-arm-eabi': 1.11.1 - '@unrs/resolver-binding-android-arm64': 1.11.1 - '@unrs/resolver-binding-darwin-arm64': 1.11.1 - '@unrs/resolver-binding-darwin-x64': 1.11.1 - '@unrs/resolver-binding-freebsd-x64': 1.11.1 - '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 - '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 - '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 - '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 - '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 - '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-x64-musl': 1.11.1 - '@unrs/resolver-binding-wasm32-wasi': 1.11.1 - '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 - '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 - '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: browserslist: 4.28.1 @@ -8093,6 +8324,10 @@ snapshots: string-width: 4.2.3 optional: true + widest-line@3.1.0: + dependencies: + string-width: 4.2.3 + word-wrap@1.2.5: {} wordwrap@1.0.0: {} @@ -8117,13 +8352,21 @@ snapshots: wrappy@1.0.2: {} + write-file-atomic@4.0.2: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + write-file-atomic@5.0.1: dependencies: imurmurhash: 0.1.4 signal-exit: 4.1.0 + optional: true ws@8.18.3: {} + xtend@4.0.2: {} + y18n@5.0.8: {} yallist@3.1.1: {} diff --git a/apps/backend/src/api-key/api-key.controller.ts b/apps/backend/src/api-key/api-key.controller.ts index eba3e404..b876b916 100644 --- a/apps/backend/src/api-key/api-key.controller.ts +++ b/apps/backend/src/api-key/api-key.controller.ts @@ -7,7 +7,16 @@ import { Param, UseGuards, Req, + HttpStatus, } from '@nestjs/common'; +import { + ApiBearerAuth, + ApiBody, + ApiOperation, + ApiParam, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; import { AuthGuard } from '../modules/auth/middleware/auth.guard'; import { ApiKeysService } from './api-key.service'; import { CreateApiKeyDto } from './dto/create-api-key.dto'; @@ -20,23 +29,38 @@ interface AuthenticatedRequest { } @Controller('api-keys') +@ApiTags('api-keys') +@ApiBearerAuth('access-token') @UseGuards(AuthGuard) export class ApiKeyController { constructor(private readonly apiKeysService: ApiKeysService) {} @Post() + @ApiOperation({ summary: 'Create an API key for the authenticated user' }) + @ApiBody({ type: CreateApiKeyDto, description: 'API key creation payload' }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'API key created successfully' }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid API key payload' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) async create(@Req() req: AuthenticatedRequest, @Body() dto: CreateApiKeyDto) { const userId = req.user.sub; return this.apiKeysService.create(userId, dto); } @Get() + @ApiOperation({ summary: 'List API keys for the authenticated user' }) + @ApiResponse({ status: HttpStatus.OK, description: 'API keys retrieved successfully' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) async list(@Req() req: AuthenticatedRequest) { const userId = req.user.sub; return this.apiKeysService.list(userId); } @Delete(':id') + @ApiOperation({ summary: 'Revoke an API key by ID' }) + @ApiParam({ name: 'id', description: 'API key ID' }) + @ApiResponse({ status: HttpStatus.OK, description: 'API key revoked successfully' }) + @ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'API key not found' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) async revoke(@Req() req: AuthenticatedRequest, @Param('id') id: string) { const userId = req.user.sub; return this.apiKeysService.revoke(id, userId); diff --git a/apps/backend/src/app-version/app-version.controller.ts b/apps/backend/src/app-version/app-version.controller.ts index 9868a6d3..b9cabf85 100644 --- a/apps/backend/src/app-version/app-version.controller.ts +++ b/apps/backend/src/app-version/app-version.controller.ts @@ -1,11 +1,15 @@ -import { Controller, Get } from '@nestjs/common'; +import { Controller, Get, HttpStatus } from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { AppVersionService, AppVersionInfo } from './app-version.service'; @Controller('api/app') +@ApiTags('app-version') export class AppVersionController { constructor(private readonly appVersionService: AppVersionService) {} @Get('version') + @ApiOperation({ summary: 'Get the current app version metadata' }) + @ApiResponse({ status: HttpStatus.OK, description: 'App version information retrieved successfully' }) getVersion(): AppVersionInfo { return this.appVersionService.getVersionInfo(); } diff --git a/apps/backend/src/app.controller.ts b/apps/backend/src/app.controller.ts index cce879ee..8e98c8fc 100644 --- a/apps/backend/src/app.controller.ts +++ b/apps/backend/src/app.controller.ts @@ -1,11 +1,15 @@ -import { Controller, Get } from '@nestjs/common'; +import { Controller, Get, HttpStatus } from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { AppService } from './app.service'; @Controller() +@ApiTags('root') export class AppController { constructor(private readonly appService: AppService) {} @Get() + @ApiOperation({ summary: 'Get the backend service welcome message' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Welcome message retrieved successfully' }) getHello(): string { return this.appService.getHello(); } diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index a88e16fb..50a88159 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -23,13 +23,36 @@ async function bootstrap() { const swaggerConfig = new DocumentBuilder() .setTitle('Vaultix Backend API') - .setDescription('Vaultix backend endpoints') + .setDescription([ + 'Vaultix backend endpoints', + '', + '## WebSocket events', + '- `connected`: emitted after successful authentication with `{ userId, socketId }`.', + '- `joinedEscrow`: emitted after joining an escrow room.', + '- `escrow:status_changed`: emitted when escrow state changes.', + '- `escrow:milestone_released`: emitted when a milestone is released.', + '- `escrow:dispute_filed` / `escrow:dispute_resolved`: emitted for dispute lifecycle updates.', + '- `notification:new`: emitted to the authenticated user when a notification is published.', + ].join('\n')) .setVersion('1.0') - .addBearerAuth() + .addBearerAuth( + { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + description: 'JWT access token issued by the auth endpoints', + }, + 'access-token', + ) .build(); const swaggerDocument = SwaggerModule.createDocument(app, swaggerConfig); - SwaggerModule.setup('api/docs', app, swaggerDocument); + SwaggerModule.setup('api/docs', app, swaggerDocument, { + customSiteTitle: 'Vaultix API Docs', + swaggerOptions: { + persistAuthorization: true, + }, + }); await app.listen(process.env.PORT ?? 3000); } diff --git a/apps/backend/src/modules/admin/admin.controller.ts b/apps/backend/src/modules/admin/admin.controller.ts index 966b9f91..d1211564 100644 --- a/apps/backend/src/modules/admin/admin.controller.ts +++ b/apps/backend/src/modules/admin/admin.controller.ts @@ -8,6 +8,14 @@ import { HttpStatus, HttpCode, } from '@nestjs/common'; +import { + ApiBearerAuth, + ApiOperation, + ApiParam, + ApiQuery, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; import { AuthGuard } from '../auth/middleware/auth.guard'; import { AdminGuard } from '../auth/middleware/admin.guard'; import { AdminService } from './admin.service'; @@ -39,6 +47,8 @@ interface PaginationQuery { } @Controller('admin') +@ApiTags('admin') +@ApiBearerAuth('access-token') @UseGuards(AuthGuard, AdminGuard) export class AdminController { constructor( @@ -47,6 +57,18 @@ export class AdminController { ) {} @Get('audit-logs') + @ApiOperation({ summary: 'List admin audit logs with optional filters' }) + @ApiQuery({ name: 'actorId', required: false, description: 'Filter by actor user ID' }) + @ApiQuery({ name: 'actionType', required: false, description: 'Filter by action type' }) + @ApiQuery({ name: 'resourceType', required: false, description: 'Filter by resource type' }) + @ApiQuery({ name: 'resourceId', required: false, description: 'Filter by resource ID' }) + @ApiQuery({ name: 'from', required: false, description: 'Filter logs created after this date' }) + @ApiQuery({ name: 'to', required: false, description: 'Filter logs created before this date' }) + @ApiQuery({ name: 'page', required: false, description: 'Page number' }) + @ApiQuery({ name: 'pageSize', required: false, description: 'Page size' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Audit logs retrieved successfully' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) + @ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Admin privileges required' }) async getAuditLogs( @Query('actorId') actorId?: string, @Query('actionType') actionType?: string, @@ -75,11 +97,22 @@ export class AdminController { } @Get('escrows') + @ApiOperation({ summary: 'List escrows across the platform for admin review' }) + @ApiQuery({ name: 'status', required: false, description: 'Filter by escrow status' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Escrows retrieved successfully' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) + @ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Admin privileges required' }) async getAllEscrows(@Query() query: EscrowQuery) { return this.adminService.getAllEscrows(query); } @Get('users') + @ApiOperation({ summary: 'List platform users for admin review' }) + @ApiQuery({ name: 'page', required: false, description: 'Page number' }) + @ApiQuery({ name: 'limit', required: false, description: 'Page size' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Users retrieved successfully' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) + @ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Admin privileges required' }) async getAllUsers(@Query() query: PaginationQuery) { const page = query.page ?? 1; const limit = query.limit ?? 20; @@ -88,12 +121,23 @@ export class AdminController { } @Get('stats') + @ApiOperation({ summary: 'Get aggregate platform statistics' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Platform stats retrieved successfully' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) + @ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Admin privileges required' }) async getStats() { return this.adminService.getPlatformStats(); } @Post('users/:id/suspend') @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Suspend a platform user account' }) + @ApiParam({ name: 'id', description: 'User ID to suspend' }) + @ApiQuery({ name: 'actorId', required: false, description: 'Admin actor identifier' }) + @ApiResponse({ status: HttpStatus.OK, description: 'User suspended successfully' }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid suspend request' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) + @ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Admin privileges required' }) async suspendUser( @Param('id') id: string, @Query('actorId') actorId?: string, diff --git a/apps/backend/src/modules/admin/controllers/admin-escrow-consistency.controller.ts b/apps/backend/src/modules/admin/controllers/admin-escrow-consistency.controller.ts index 87a92072..54374fc3 100644 --- a/apps/backend/src/modules/admin/controllers/admin-escrow-consistency.controller.ts +++ b/apps/backend/src/modules/admin/controllers/admin-escrow-consistency.controller.ts @@ -1,4 +1,11 @@ -import { Body, Controller, Post, UseGuards } from '@nestjs/common'; +import { Body, Controller, Post, UseGuards, HttpStatus } from '@nestjs/common'; +import { + ApiBearerAuth, + ApiBody, + ApiOperation, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; import { ConsistencyCheckerService } from '../services/consistency-checker.service'; import { ConsistencyCheckRequest, @@ -7,11 +14,19 @@ import { import { AdminGuard } from '../../auth/middleware/admin.guard'; @Controller('admin/escrows') +@ApiTags('admin/escrows') +@ApiBearerAuth('access-token') @UseGuards(AdminGuard) export class AdminEscrowConsistencyController { constructor(private readonly checker: ConsistencyCheckerService) {} @Post('consistency-check') + @ApiOperation({ summary: 'Run an escrow consistency check across the database and on-chain state' }) + @ApiBody({ description: 'Consistency check request payload', schema: { oneOf: [{ type: 'object', properties: { escrowIds: { type: 'array', items: { type: 'number' } } } }, { type: 'object', properties: { fromId: { type: 'number' }, toId: { type: 'number' } } }] } }) + @ApiResponse({ status: HttpStatus.OK, description: 'Consistency check completed' }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid consistency check payload' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) + @ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Admin privileges required' }) async checkConsistency( @Body() body: ConsistencyCheckRequest, ): Promise { diff --git a/apps/backend/src/modules/assets/admin-assets.controller.ts b/apps/backend/src/modules/assets/admin-assets.controller.ts index e79f790f..5e895ae3 100644 --- a/apps/backend/src/modules/assets/admin-assets.controller.ts +++ b/apps/backend/src/modules/assets/admin-assets.controller.ts @@ -7,38 +7,79 @@ import { Param, Delete, UseGuards, + HttpStatus, } from '@nestjs/common'; +import { + ApiBearerAuth, + ApiBody, + ApiOperation, + ApiParam, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; import { AssetsService } from './assets.service'; import { CreateAssetDto, UpdateAssetDto } from './dto/asset.dto'; import { AuthGuard } from '../auth/middleware/auth.guard'; import { AdminGuard } from '../auth/middleware/admin.guard'; @Controller('admin/assets') +@ApiTags('admin/assets') +@ApiBearerAuth('access-token') @UseGuards(AuthGuard, AdminGuard) export class AdminAssetsController { constructor(private readonly assetsService: AssetsService) {} @Post() + @ApiOperation({ summary: 'Create a new asset' }) + @ApiBody({ type: CreateAssetDto, description: 'Asset creation payload' }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Asset created successfully' }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid asset payload' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) + @ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Admin privileges required' }) create(@Body() createAssetDto: CreateAssetDto) { return this.assetsService.create(createAssetDto); } @Get() + @ApiOperation({ summary: 'List all assets including inactive ones' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Assets retrieved successfully' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) + @ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Admin privileges required' }) findAll() { return this.assetsService.findAll(false); } @Get(':id') + @ApiOperation({ summary: 'Get an asset by ID' }) + @ApiParam({ name: 'id', description: 'Asset ID' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Asset retrieved successfully' }) + @ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'Asset not found' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) + @ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Admin privileges required' }) findOne(@Param('id') id: string) { return this.assetsService.findOne(id); } @Patch(':id') + @ApiOperation({ summary: 'Update an existing asset' }) + @ApiParam({ name: 'id', description: 'Asset ID' }) + @ApiBody({ type: UpdateAssetDto, description: 'Asset update payload' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Asset updated successfully' }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid asset payload' }) + @ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'Asset not found' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) + @ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Admin privileges required' }) update(@Param('id') id: string, @Body() updateAssetDto: UpdateAssetDto) { return this.assetsService.update(id, updateAssetDto); } @Delete(':id') + @ApiOperation({ summary: 'Delete an asset by ID' }) + @ApiParam({ name: 'id', description: 'Asset ID' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Asset deleted successfully' }) + @ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'Asset not found' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) + @ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Admin privileges required' }) remove(@Param('id') id: string) { return this.assetsService.remove(id); } diff --git a/apps/backend/src/modules/assets/assets.controller.ts b/apps/backend/src/modules/assets/assets.controller.ts index 541f26d5..4c7366dc 100644 --- a/apps/backend/src/modules/assets/assets.controller.ts +++ b/apps/backend/src/modules/assets/assets.controller.ts @@ -9,7 +9,13 @@ import { import { Request as ExpressRequest } from 'express'; import { AssetsService } from './assets.service'; import { AuthGuard } from '../auth/middleware/auth.guard'; -import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { + ApiBearerAuth, + ApiOperation, + ApiQuery, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; interface AuthenticatedRequest extends ExpressRequest { user: { @@ -26,13 +32,21 @@ export class AssetsController { constructor(private readonly assetsService: AssetsService) {} @Get() + @ApiOperation({ summary: 'List all active assets' }) + @ApiResponse({ status: 200, description: 'Assets retrieved successfully' }) async findAllActive() { return this.assetsService.findAll(true); } @Get('balance') @UseGuards(AuthGuard) - @ApiBearerAuth() + @ApiBearerAuth('access-token') + @ApiOperation({ summary: 'Get the authenticated wallet balance for an asset' }) + @ApiQuery({ name: 'assetCode', required: true, description: 'Asset code to retrieve the balance for' }) + @ApiQuery({ name: 'issuer', required: false, description: 'Asset issuer for non-native assets' }) + @ApiResponse({ status: 200, description: 'Asset balance retrieved successfully' }) + @ApiResponse({ status: 400, description: 'Asset code is required' }) + @ApiResponse({ status: 401, description: 'Authentication required' }) async getBalance( @Query('assetCode') assetCode: string, @Query('issuer') issuer: string | undefined, diff --git a/apps/backend/src/modules/assets/dto/asset.dto.ts b/apps/backend/src/modules/assets/dto/asset.dto.ts index e8e481e9..4a1a0f9c 100644 --- a/apps/backend/src/modules/assets/dto/asset.dto.ts +++ b/apps/backend/src/modules/assets/dto/asset.dto.ts @@ -8,62 +8,75 @@ import { IsBoolean, Length, } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class CreateAssetDto { + @ApiProperty({ description: 'Asset code that identifies the asset', example: 'USDC' }) @IsString() @IsNotEmpty() @Length(1, 12) code: string; + @ApiPropertyOptional({ description: 'Issuer account for non-native assets', example: 'GA...' }) @IsString() @IsOptional() @Length(56, 56) issuer?: string; + @ApiProperty({ description: 'Human-readable asset name', example: 'USD Coin' }) @IsString() @IsNotEmpty() displayName: string; + @ApiPropertyOptional({ description: 'URL to the asset icon', example: 'https://example.com/usdc.png' }) @IsString() @IsOptional() iconUrl?: string; + @ApiPropertyOptional({ description: 'Number of decimal places for the asset', example: 7 }) @IsInt() @Min(0) @Max(18) @IsOptional() decimals?: number; + @ApiPropertyOptional({ description: 'Whether the asset is active', example: true }) @IsBoolean() @IsOptional() active?: boolean; } export class UpdateAssetDto { + @ApiPropertyOptional({ description: 'Asset code that identifies the asset', example: 'USDC' }) @IsString() @IsOptional() @Length(1, 12) code?: string; + @ApiPropertyOptional({ description: 'Issuer account for non-native assets', example: 'GA...' }) @IsString() @IsOptional() @Length(56, 56) issuer?: string; + @ApiPropertyOptional({ description: 'Human-readable asset name', example: 'USD Coin' }) @IsString() @IsOptional() displayName?: string; + @ApiPropertyOptional({ description: 'URL to the asset icon', example: 'https://example.com/usdc.png' }) @IsString() @IsOptional() iconUrl?: string; + @ApiPropertyOptional({ description: 'Number of decimal places for the asset', example: 7 }) @IsInt() @Min(0) @Max(18) @IsOptional() decimals?: number; + @ApiPropertyOptional({ description: 'Whether the asset is active', example: true }) @IsBoolean() @IsOptional() active?: boolean; diff --git a/apps/backend/src/modules/auth/controllers/auth.controller.ts b/apps/backend/src/modules/auth/controllers/auth.controller.ts index ae5c9c08..4f3cd9c5 100644 --- a/apps/backend/src/modules/auth/controllers/auth.controller.ts +++ b/apps/backend/src/modules/auth/controllers/auth.controller.ts @@ -15,6 +15,15 @@ import { import { FileInterceptor } from '@nestjs/platform-express'; import { Request } from 'express'; import { Throttle } from '@nestjs/throttler'; +import { + ApiBearerAuth, + ApiBody, + ApiOperation, + ApiParam, + ApiQuery, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; import { AuthService } from '../services/auth.service'; import { ChallengeDto, @@ -27,6 +36,7 @@ import { AuthGuard } from '../middleware/auth.guard'; import { AuthThrottlerGuard } from '../middleware/auth-throttler.guard'; @Controller('auth') +@ApiTags('auth') @UseGuards(AuthThrottlerGuard) export class AuthController { constructor(private readonly authService: AuthService) {} @@ -34,6 +44,21 @@ export class AuthController { @Post('challenge') @HttpCode(HttpStatus.OK) @Throttle({ default: { limit: 10, ttl: 60000 } }) + @ApiOperation({ summary: 'Generate a signed challenge for wallet authentication' }) + @ApiBody({ + type: ChallengeDto, + description: 'Wallet address used to request a challenge', + examples: { + default: { + summary: 'Example request', + value: { walletAddress: 'GABC1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF' }, + }, + }, + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Challenge generated successfully' }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid wallet address' }) + @ApiResponse({ status: HttpStatus.TOO_MANY_REQUESTS, description: 'Too many requests' }) + @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, description: 'Unexpected error' }) async challenge(@Body() challengeDto: ChallengeDto) { return this.authService.generateChallenge(challengeDto.walletAddress); } @@ -41,6 +66,25 @@ export class AuthController { @Post('verify') @HttpCode(HttpStatus.OK) @Throttle({ default: { limit: 5, ttl: 60000 } }) + @ApiOperation({ summary: 'Verify a signed challenge and issue tokens' }) + @ApiBody({ + type: VerifyDto, + description: 'Signature and public key used to verify authentication', + examples: { + default: { + summary: 'Example request', + value: { + signature: 'signature', + publicKey: 'GABC1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF', + }, + }, + }, + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Authentication verified successfully' }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid signature payload' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Signature verification failed' }) + @ApiResponse({ status: HttpStatus.TOO_MANY_REQUESTS, description: 'Too many requests' }) + @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, description: 'Unexpected error' }) async verify(@Body() verifyDto: VerifyDto) { return this.authService.verifySignature( verifyDto.signature, @@ -51,12 +95,23 @@ export class AuthController { @Post('refresh') @HttpCode(HttpStatus.OK) @Throttle({ default: { limit: 20, ttl: 60000 } }) + @ApiOperation({ summary: 'Refresh an access token using a refresh token' }) + @ApiBody({ type: RefreshTokenDto, description: 'Refresh token to exchange' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Tokens refreshed successfully' }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid refresh token' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Refresh token expired or invalid' }) + @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, description: 'Unexpected error' }) async refresh(@Body() refreshTokenDto: RefreshTokenDto) { return this.authService.refreshAccessToken(refreshTokenDto.refreshToken); } @Get('me') @UseGuards(AuthGuard) + @ApiBearerAuth('access-token') + @ApiOperation({ summary: 'Get the current authenticated user profile' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Current user retrieved successfully' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) + @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, description: 'Unexpected error' }) async getCurrentUser(@Req() req: Request & { user: { userId: string } }) { const user = await this.authService.getCurrentUser(req.user.userId); return { @@ -75,6 +130,13 @@ export class AuthController { @Patch('profile') @UseGuards(AuthGuard) + @ApiBearerAuth('access-token') + @ApiOperation({ summary: 'Update the authenticated user profile' }) + @ApiBody({ type: UpdateProfileDto, description: 'Profile fields to update' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Profile updated successfully' }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid profile payload' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) + @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, description: 'Unexpected error' }) async updateProfile( @Req() req: Request & { user: { userId: string } }, @Body() updateProfileDto: UpdateProfileDto, @@ -97,7 +159,13 @@ export class AuthController { @Post('profile/avatar') @UseGuards(AuthGuard) + @ApiBearerAuth('access-token') @UseInterceptors(FileInterceptor('avatar')) + @ApiOperation({ summary: 'Upload a profile avatar for the authenticated user' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Avatar uploaded successfully' }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid avatar upload' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) + @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, description: 'Unexpected error' }) async uploadAvatar( @Req() req: Request & { user: { userId: string } }, @UploadedFile() file: { buffer: Buffer; originalname: string }, @@ -111,7 +179,12 @@ export class AuthController { @Post('profile/verify-email') @UseGuards(AuthGuard) + @ApiBearerAuth('access-token') @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Send an email verification message to the authenticated user' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Verification email sent' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) + @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, description: 'Unexpected error' }) async sendEmailVerification( @Req() req: Request & { user: { userId: string } }, ) { @@ -120,6 +193,11 @@ export class AuthController { } @Get('profile/verify-email') + @ApiOperation({ summary: 'Verify a pending email address using a token' }) + @ApiQuery({ name: 'token', required: true, description: 'Verification token' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Email verified successfully' }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid or expired verification token' }) + @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, description: 'Unexpected error' }) async verifyEmail(@Query('token') token: string) { await this.authService.verifyEmail(token); return { message: 'Email verified successfully' }; @@ -127,8 +205,15 @@ export class AuthController { @Post('logout') @UseGuards(AuthGuard) + @ApiBearerAuth('access-token') @HttpCode(HttpStatus.OK) @Throttle({ default: { limit: 10, ttl: 60000 } }) + @ApiOperation({ summary: 'Log out an authenticated user and revoke the refresh token' }) + @ApiBody({ type: LogoutDto, description: 'Refresh token to revoke' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Successfully logged out' }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid logout payload' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) + @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, description: 'Unexpected error' }) async logout(@Body() logoutDto: LogoutDto) { await this.authService.logout(logoutDto.refreshToken); return { message: 'Successfully logged out' }; diff --git a/apps/backend/src/modules/auth/dto/auth.dto.ts b/apps/backend/src/modules/auth/dto/auth.dto.ts index 907951db..f6d7fc70 100644 --- a/apps/backend/src/modules/auth/dto/auth.dto.ts +++ b/apps/backend/src/modules/auth/dto/auth.dto.ts @@ -1,6 +1,11 @@ import { IsString, IsNotEmpty, Length, Matches } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; export class ChallengeDto { + @ApiProperty({ + description: 'Stellar wallet address used to start authentication', + example: 'GABC1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF', + }) @IsString() @IsNotEmpty() @Length(1, 56) @@ -9,10 +14,15 @@ export class ChallengeDto { } export class VerifyDto { + @ApiProperty({ description: 'Base64 or hex-encoded signature for the challenge', example: 'signature' }) @IsString() @IsNotEmpty() signature: string; + @ApiProperty({ + description: 'Stellar public key for the signing wallet', + example: 'GABC1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF', + }) @IsString() @IsNotEmpty() @Length(1, 56) @@ -21,12 +31,14 @@ export class VerifyDto { } export class RefreshTokenDto { + @ApiProperty({ description: 'Refresh token used to mint a new access token', example: 'refresh-token' }) @IsString() @IsNotEmpty() refreshToken: string; } export class LogoutDto { + @ApiProperty({ description: 'Refresh token to revoke during logout', example: 'refresh-token' }) @IsString() @IsNotEmpty() refreshToken: string; diff --git a/apps/backend/src/modules/auth/dto/profile.dto.ts b/apps/backend/src/modules/auth/dto/profile.dto.ts index 031e5c64..2c61d9d6 100644 --- a/apps/backend/src/modules/auth/dto/profile.dto.ts +++ b/apps/backend/src/modules/auth/dto/profile.dto.ts @@ -5,26 +5,32 @@ import { IsString, MaxLength, } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; export class UpdateProfileDto { + @ApiPropertyOptional({ description: 'Display name shown on the profile', example: 'Vaultix User' }) @IsOptional() @IsString() @MaxLength(100) displayName?: string; + @ApiPropertyOptional({ description: 'Email address for the account', example: 'user@example.com' }) @IsOptional() @IsEmail() @MaxLength(255) email?: string; + @ApiPropertyOptional({ description: 'Whether the address has been verified', example: false }) @IsOptional() @IsBoolean() emailVerified?: boolean; + @ApiPropertyOptional({ description: 'Short biography for the profile', example: 'Building escrow workflows' }) @IsOptional() @IsString() bio?: string; + @ApiPropertyOptional({ description: 'Preferred asset code', example: 'XLM' }) @IsOptional() @IsString() @MaxLength(20) diff --git a/apps/backend/src/modules/escrow/controllers/escrow-scheduler.controller.ts b/apps/backend/src/modules/escrow/controllers/escrow-scheduler.controller.ts index 56f5b6ea..71cef93f 100644 --- a/apps/backend/src/modules/escrow/controllers/escrow-scheduler.controller.ts +++ b/apps/backend/src/modules/escrow/controllers/escrow-scheduler.controller.ts @@ -6,17 +6,30 @@ import { HttpStatus, HttpCode, } from '@nestjs/common'; +import { + ApiBearerAuth, + ApiOperation, + ApiParam, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; import { AuthGuard } from '../../auth/middleware/auth.guard'; import { AdminGuard } from '../../auth/middleware/admin.guard'; import { EscrowSchedulerService } from '../services/escrow-scheduler.service'; @Controller('escrows/scheduler') +@ApiTags('escrows/scheduler') +@ApiBearerAuth('access-token') @UseGuards(AuthGuard, AdminGuard) export class EscrowSchedulerController { constructor(private readonly schedulerService: EscrowSchedulerService) {} @Post('process-expired') @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Process expired escrows' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Expired escrows processing initiated' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) + @ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Admin privileges required' }) async processExpiredEscrows() { await this.schedulerService.handleExpiredEscrows(); return { message: 'Expired escrow processing initiated' }; @@ -24,6 +37,10 @@ export class EscrowSchedulerController { @Post('send-warnings') @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Send expiration warnings for escrows approaching their deadline' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Expiration warnings queued successfully' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) + @ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Admin privileges required' }) async sendExpirationWarnings() { await this.schedulerService.sendExpirationWarnings(); return { message: 'Expiration warning sending initiated' }; @@ -31,6 +48,12 @@ export class EscrowSchedulerController { @Post('process/:escrowId') @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Process a specific escrow manually' }) + @ApiParam({ name: 'escrowId', description: 'Escrow ID to process' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Escrow processing initiated' }) + @ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'Escrow not found' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) + @ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Admin privileges required' }) async processEscrowManually(@Param('escrowId') escrowId: string) { await this.schedulerService.processEscrowManually(escrowId); return { message: `Escrow ${escrowId} processed manually` }; diff --git a/apps/backend/src/modules/escrow/controllers/escrow.controller.ts b/apps/backend/src/modules/escrow/controllers/escrow.controller.ts index b999b8b5..ed4b4a8e 100644 --- a/apps/backend/src/modules/escrow/controllers/escrow.controller.ts +++ b/apps/backend/src/modules/escrow/controllers/escrow.controller.ts @@ -21,8 +21,12 @@ import { ThrottlerGuard } from '@nestjs/throttler'; import { Request as ExpressRequest } from 'express'; import { ApiBearerAuth, + ApiBody, ApiOkResponse, ApiOperation, + ApiParam, + ApiQuery, + ApiResponse, ApiTags, } from '@nestjs/swagger'; import { AuthGuard } from '../../auth/middleware/auth.guard'; @@ -50,7 +54,7 @@ interface AuthenticatedRequest extends ExpressRequest { @Controller('escrows') @ApiTags('escrows') -@ApiBearerAuth() +@ApiBearerAuth('access-token') @UseGuards(ThrottlerGuard, AuthGuard) export class EscrowController { constructor(private readonly escrowService: EscrowService) {} @@ -65,6 +69,12 @@ export class EscrowController { } @Post() + @ApiOperation({ summary: 'Create a new escrow' }) + @ApiBody({ type: CreateEscrowDto, description: 'Escrow creation payload' }) + @ApiResponse({ status: 201, description: 'Escrow created successfully' }) + @ApiResponse({ status: 400, description: 'Invalid escrow payload' }) + @ApiResponse({ status: 401, description: 'Authentication required' }) + @ApiResponse({ status: 409, description: 'Escrow conflicts with an existing resource' }) async create( @Body() dto: CreateEscrowDto, @Request() req: AuthenticatedRequest, @@ -75,6 +85,12 @@ export class EscrowController { } @Get() + @ApiOperation({ summary: 'List escrows for the authenticated user' }) + @ApiQuery({ name: 'page', required: false, description: 'Page number' }) + @ApiQuery({ name: 'limit', required: false, description: 'Page size' }) + @ApiQuery({ name: 'status', required: false, description: 'Filter by escrow status' }) + @ApiResponse({ status: 200, description: 'Escrows retrieved successfully' }) + @ApiResponse({ status: 401, description: 'Authentication required' }) async findAll( @Query() query: ListEscrowsDto, @Request() req: AuthenticatedRequest, @@ -109,12 +125,26 @@ export class EscrowController { @Get(':id') @UseGuards(EscrowAccessGuard) + @ApiOperation({ summary: 'Get a single escrow by ID' }) + @ApiParam({ name: 'id', description: 'Escrow ID' }) + @ApiResponse({ status: 200, description: 'Escrow retrieved successfully' }) + @ApiResponse({ status: 401, description: 'Authentication required' }) + @ApiResponse({ status: 403, description: 'Access denied' }) + @ApiResponse({ status: 404, description: 'Escrow not found' }) async findOne(@Param('id') id: string) { return this.escrowService.findOne(id); } @Patch(':id') @UseGuards(EscrowAccessGuard) + @ApiOperation({ summary: 'Update an escrow by ID' }) + @ApiParam({ name: 'id', description: 'Escrow ID' }) + @ApiBody({ type: UpdateEscrowDto, description: 'Escrow update payload' }) + @ApiResponse({ status: 200, description: 'Escrow updated successfully' }) + @ApiResponse({ status: 400, description: 'Invalid update payload' }) + @ApiResponse({ status: 401, description: 'Authentication required' }) + @ApiResponse({ status: 403, description: 'Access denied' }) + @ApiResponse({ status: 404, description: 'Escrow not found' }) async update( @Param('id') id: string, @Body() dto: UpdateEscrowDto, @@ -127,6 +157,14 @@ export class EscrowController { @Post(':id/cancel') @UseGuards(EscrowAccessGuard) + @ApiOperation({ summary: 'Cancel an escrow by ID' }) + @ApiParam({ name: 'id', description: 'Escrow ID' }) + @ApiBody({ type: CancelEscrowDto, description: 'Escrow cancellation payload' }) + @ApiResponse({ status: 200, description: 'Escrow cancelled successfully' }) + @ApiResponse({ status: 400, description: 'Invalid cancellation payload' }) + @ApiResponse({ status: 401, description: 'Authentication required' }) + @ApiResponse({ status: 403, description: 'Access denied' }) + @ApiResponse({ status: 404, description: 'Escrow not found' }) async cancel( @Param('id') id: string, @Body() dto: CancelEscrowDto, @@ -139,6 +177,14 @@ export class EscrowController { @Post(':id/expire') @UseGuards(EscrowExpireGuard) + @ApiOperation({ summary: 'Expire an escrow by ID' }) + @ApiParam({ name: 'id', description: 'Escrow ID' }) + @ApiBody({ type: ExpireEscrowDto, description: 'Escrow expiration payload' }) + @ApiResponse({ status: 200, description: 'Escrow expired successfully' }) + @ApiResponse({ status: 400, description: 'Invalid expiration payload' }) + @ApiResponse({ status: 401, description: 'Authentication required' }) + @ApiResponse({ status: 403, description: 'Access denied' }) + @ApiResponse({ status: 404, description: 'Escrow not found' }) async expire( @Param('id') id: string, @Body() dto: ExpireEscrowDto, @@ -152,6 +198,14 @@ export class EscrowController { @Get(':id/events') @UseGuards(EscrowAccessGuard) + @ApiOperation({ summary: 'List events for an escrow' }) + @ApiParam({ name: 'id', description: 'Escrow ID' }) + @ApiQuery({ name: 'page', required: false, description: 'Page number' }) + @ApiQuery({ name: 'limit', required: false, description: 'Page size' }) + @ApiResponse({ status: 200, description: 'Escrow events retrieved successfully' }) + @ApiResponse({ status: 401, description: 'Authentication required' }) + @ApiResponse({ status: 403, description: 'Access denied' }) + @ApiResponse({ status: 404, description: 'Escrow not found' }) async findEscrowEvents( @Param('id') id: string, @Query() query: ListEventsDto, @@ -163,6 +217,14 @@ export class EscrowController { @Post(':id/fund') @UseGuards(EscrowAccessGuard) + @ApiOperation({ summary: 'Fund an escrow' }) + @ApiParam({ name: 'id', description: 'Escrow ID' }) + @ApiBody({ type: FundEscrowDto, description: 'Funding request payload' }) + @ApiResponse({ status: 200, description: 'Escrow funded successfully' }) + @ApiResponse({ status: 400, description: 'Invalid funding payload' }) + @ApiResponse({ status: 401, description: 'Authentication required' }) + @ApiResponse({ status: 403, description: 'Access denied' }) + @ApiResponse({ status: 404, description: 'Escrow not found' }) async fund( @Param('id') id: string, @Body() dto: FundEscrowDto, @@ -180,6 +242,12 @@ export class EscrowController { @Post(':id/release') @UseGuards(AuthGuard) + @ApiOperation({ summary: 'Release funds for an escrow' }) + @ApiParam({ name: 'id', description: 'Escrow ID' }) + @ApiResponse({ status: 200, description: 'Escrow released successfully' }) + @ApiResponse({ status: 401, description: 'Authentication required' }) + @ApiResponse({ status: 403, description: 'Access denied' }) + @ApiResponse({ status: 404, description: 'Escrow not found' }) async releaseEscrow( @Param('id') id: string, @Req() req: AuthenticatedRequest, @@ -199,6 +267,15 @@ export class EscrowController { @Post(':id/conditions/:conditionId/fulfill') @UseGuards(EscrowAccessGuard) + @ApiOperation({ summary: 'Fulfill an escrow condition' }) + @ApiParam({ name: 'id', description: 'Escrow ID' }) + @ApiParam({ name: 'conditionId', description: 'Condition ID' }) + @ApiBody({ type: FulfillConditionDto, description: 'Condition fulfillment payload' }) + @ApiResponse({ status: 200, description: 'Condition fulfilled successfully' }) + @ApiResponse({ status: 400, description: 'Invalid fulfillment payload' }) + @ApiResponse({ status: 401, description: 'Authentication required' }) + @ApiResponse({ status: 403, description: 'Access denied' }) + @ApiResponse({ status: 404, description: 'Condition or escrow not found' }) async fulfillCondition( @Param('id') escrowId: string, @Param('conditionId') conditionId: string, @@ -218,6 +295,13 @@ export class EscrowController { @Post(':id/conditions/:conditionId/confirm') @UseGuards(EscrowAccessGuard) + @ApiOperation({ summary: 'Confirm a fulfilled condition' }) + @ApiParam({ name: 'id', description: 'Escrow ID' }) + @ApiParam({ name: 'conditionId', description: 'Condition ID' }) + @ApiResponse({ status: 200, description: 'Condition confirmed successfully' }) + @ApiResponse({ status: 401, description: 'Authentication required' }) + @ApiResponse({ status: 403, description: 'Access denied' }) + @ApiResponse({ status: 404, description: 'Condition or escrow not found' }) async confirmCondition( @Param('id') escrowId: string, @Param('conditionId') conditionId: string, @@ -321,6 +405,14 @@ export class EscrowController { */ @Post(':id/dispute') @UseGuards(EscrowAccessGuard) + @ApiOperation({ summary: 'File a dispute for an escrow' }) + @ApiParam({ name: 'id', description: 'Escrow ID' }) + @ApiBody({ type: FileDisputeDto, description: 'Dispute filing payload' }) + @ApiResponse({ status: 200, description: 'Dispute filed successfully' }) + @ApiResponse({ status: 400, description: 'Invalid dispute payload' }) + @ApiResponse({ status: 401, description: 'Authentication required' }) + @ApiResponse({ status: 403, description: 'Access denied' }) + @ApiResponse({ status: 404, description: 'Escrow not found' }) async fileDispute( @Param('id') id: string, @Body() dto: FileDisputeDto, @@ -341,6 +433,12 @@ export class EscrowController { */ @Get(':id/dispute') @UseGuards(EscrowAccessGuard) + @ApiOperation({ summary: 'Get the dispute record for an escrow' }) + @ApiParam({ name: 'id', description: 'Escrow ID' }) + @ApiResponse({ status: 200, description: 'Dispute retrieved successfully' }) + @ApiResponse({ status: 401, description: 'Authentication required' }) + @ApiResponse({ status: 403, description: 'Access denied' }) + @ApiResponse({ status: 404, description: 'Escrow or dispute not found' }) async getDispute(@Param('id') id: string) { return this.escrowService.getDispute(id); } @@ -352,6 +450,14 @@ export class EscrowController { */ @Post(':id/dispute/resolve') @UseGuards(EscrowAccessGuard) + @ApiOperation({ summary: 'Resolve an escrow dispute' }) + @ApiParam({ name: 'id', description: 'Escrow ID' }) + @ApiBody({ type: ResolveDisputeDto, description: 'Dispute resolution payload' }) + @ApiResponse({ status: 200, description: 'Dispute resolved successfully' }) + @ApiResponse({ status: 400, description: 'Invalid dispute resolution payload' }) + @ApiResponse({ status: 401, description: 'Authentication required' }) + @ApiResponse({ status: 403, description: 'Access denied' }) + @ApiResponse({ status: 404, description: 'Escrow or dispute not found' }) async resolveDispute( @Param('id') id: string, @Body() dto: ResolveDisputeDto, @@ -368,6 +474,13 @@ export class EscrowController { @Post(':id/evidence') @UseGuards(EscrowAccessGuard) @UseInterceptors(FileInterceptor('file')) + @ApiOperation({ summary: 'Upload evidence for an escrow' }) + @ApiParam({ name: 'id', description: 'Escrow ID' }) + @ApiResponse({ status: 201, description: 'Evidence uploaded successfully' }) + @ApiResponse({ status: 400, description: 'Invalid evidence file upload' }) + @ApiResponse({ status: 401, description: 'Authentication required' }) + @ApiResponse({ status: 403, description: 'Access denied' }) + @ApiResponse({ status: 404, description: 'Escrow not found' }) async uploadEvidence( @Param('id') id: string, @Request() req: AuthenticatedRequest, diff --git a/apps/backend/src/modules/escrow/controllers/events.controller.ts b/apps/backend/src/modules/escrow/controllers/events.controller.ts index 406ca925..8b86480b 100644 --- a/apps/backend/src/modules/escrow/controllers/events.controller.ts +++ b/apps/backend/src/modules/escrow/controllers/events.controller.ts @@ -1,6 +1,7 @@ -import { Controller, Get, Query, UseGuards, Request } from '@nestjs/common'; +import { Controller, Get, Query, UseGuards, Request, HttpStatus } from '@nestjs/common'; import { ThrottlerGuard } from '@nestjs/throttler'; import { Request as ExpressRequest } from 'express'; +import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { AuthGuard } from '../../auth/middleware/auth.guard'; import { EscrowService } from '../services/escrow.service'; import { ListEventsDto } from '../dto/list-events.dto'; @@ -10,11 +11,16 @@ interface AuthenticatedRequest extends ExpressRequest { } @Controller('events') +@ApiTags('events') +@ApiBearerAuth('access-token') @UseGuards(ThrottlerGuard, AuthGuard) export class EventsController { constructor(private readonly escrowService: EscrowService) {} @Get() + @ApiOperation({ summary: 'List escrow-related events for the authenticated user' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Events retrieved successfully' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) async findAllEvents( @Query() query: ListEventsDto, @Request() req: AuthenticatedRequest, diff --git a/apps/backend/src/modules/escrow/dto/cancel-escrow.dto.ts b/apps/backend/src/modules/escrow/dto/cancel-escrow.dto.ts index 50e13045..8504fc88 100644 --- a/apps/backend/src/modules/escrow/dto/cancel-escrow.dto.ts +++ b/apps/backend/src/modules/escrow/dto/cancel-escrow.dto.ts @@ -1,6 +1,8 @@ import { IsString, IsOptional, MaxLength } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; export class CancelEscrowDto { + @ApiPropertyOptional({ description: 'Reason for canceling the escrow', example: 'The buyer no longer needs the service' }) @IsString() @IsOptional() @MaxLength(1000) diff --git a/apps/backend/src/modules/escrow/dto/create-escrow.dto.ts b/apps/backend/src/modules/escrow/dto/create-escrow.dto.ts index 76f7c2b8..d6f65fc4 100644 --- a/apps/backend/src/modules/escrow/dto/create-escrow.dto.ts +++ b/apps/backend/src/modules/escrow/dto/create-escrow.dto.ts @@ -13,15 +13,18 @@ import { ValidateIf, } from 'class-validator'; import { Type } from 'class-transformer'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { EscrowType } from '../entities/escrow.entity'; import { PartyRole } from '../entities/party.entity'; import { ConditionType } from '../entities/condition.entity'; export class EscrowAssetDto { + @ApiProperty({ description: 'Asset code to escrow', example: 'XLM' }) @IsString() @IsNotEmpty() code: string; + @ApiProperty({ description: 'Issuer for non-native assets', example: 'GA...' }) @ValidateIf((o: EscrowAssetDto) => o.code !== 'XLM') @IsString() @IsNotEmpty() @@ -29,68 +32,82 @@ export class EscrowAssetDto { } export class CreatePartyDto { + @ApiProperty({ description: 'User identifier for the party', example: 'user_123' }) @IsString() @IsNotEmpty() userId: string; + @ApiProperty({ enum: PartyRole, description: 'Role of the party in the escrow' }) @IsEnum(PartyRole) role: PartyRole; } export class CreateConditionDto { + @ApiProperty({ description: 'Condition description', example: 'Deliver the signed contract' }) @IsString() @IsNotEmpty() @MaxLength(1000) description: string; + @ApiPropertyOptional({ enum: ConditionType, description: 'Condition type' }) @IsEnum(ConditionType) @IsOptional() type?: ConditionType; + @ApiPropertyOptional({ description: 'Arbitrary metadata for the condition' }) @IsOptional() metadata?: Record; } export class CreateEscrowDto { + @ApiProperty({ description: 'Escrow title', example: 'Website redesign' }) @IsString() @IsNotEmpty() @MaxLength(255) title: string; + @ApiPropertyOptional({ description: 'Escrow description', example: 'Create a new landing page' }) @IsString() @IsOptional() @MaxLength(2000) description?: string; + @ApiProperty({ description: 'Escrow amount in the selected asset', example: 1000 }) @IsNumber() @IsPositive() amount: number; + @ApiPropertyOptional({ type: EscrowAssetDto, description: 'Asset used for the escrow' }) @IsOptional() @ValidateNested() @Type(() => EscrowAssetDto) asset?: EscrowAssetDto; + @ApiPropertyOptional({ enum: EscrowType, description: 'Escrow type' }) @IsEnum(EscrowType) @IsOptional() type?: EscrowType; + @ApiProperty({ type: [CreatePartyDto], description: 'Parties participating in the escrow' }) @IsArray() @ArrayMinSize(1) @ValidateNested({ each: true }) @Type(() => CreatePartyDto) parties: CreatePartyDto[]; + @ApiPropertyOptional({ type: [CreateConditionDto], description: 'Conditions attached to the escrow' }) @IsArray() @IsOptional() @ValidateNested({ each: true }) @Type(() => CreateConditionDto) conditions?: CreateConditionDto[]; + @ApiPropertyOptional({ description: 'Optional expiry date for the escrow', example: '2026-12-31T23:59:59.000Z' }) @IsDateString() @IsOptional() expiresAt?: string; + @ApiPropertyOptional({ description: 'Optional metadata hash', example: 'ipfs://abc123' }) @IsString() @IsOptional() metadataHash?: string; diff --git a/apps/backend/src/modules/escrow/dto/dispute.dto.ts b/apps/backend/src/modules/escrow/dto/dispute.dto.ts index 0ec7f2cc..aee0c6e8 100644 --- a/apps/backend/src/modules/escrow/dto/dispute.dto.ts +++ b/apps/backend/src/modules/escrow/dto/dispute.dto.ts @@ -9,9 +9,11 @@ import { Min, Max, } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { DisputeOutcome } from '../entities/dispute.entity'; export class FileDisputeDto { + @ApiProperty({ description: 'Reason for filing the dispute', example: 'The work was not delivered on time' }) @IsString() @IsNotEmpty() @MaxLength(2000) @@ -21,6 +23,7 @@ export class FileDisputeDto { * Optional list of evidence URLs or reference strings (e.g. IPFS CIDs, * cloud storage links, transaction hashes). */ + @ApiPropertyOptional({ description: 'Optional evidence references for the dispute', example: ['ipfs://abc123'] }) @IsArray() @IsOptional() @IsString({ each: true }) @@ -29,9 +32,11 @@ export class FileDisputeDto { } export class ResolveDisputeDto { + @ApiProperty({ enum: DisputeOutcome, description: 'Resolution outcome for the dispute' }) @IsEnum(DisputeOutcome) outcome: DisputeOutcome; + @ApiProperty({ description: 'Notes explaining the dispute resolution', example: 'The buyer was refunded' }) @IsString() @IsNotEmpty() @MaxLength(2000) @@ -41,6 +46,7 @@ export class ResolveDisputeDto { * Percentage of funds to release to the seller (0-100). * Required when outcome is SPLIT; sellerPercent + buyerPercent must equal 100. */ + @ApiPropertyOptional({ description: 'Percentage of funds released to the seller', example: 50 }) @IsNumber() @Min(0) @Max(100) @@ -51,6 +57,7 @@ export class ResolveDisputeDto { * Percentage of funds to refund to the buyer (0-100). * Required when outcome is SPLIT; sellerPercent + buyerPercent must equal 100. */ + @ApiPropertyOptional({ description: 'Percentage of funds refunded to the buyer', example: 50 }) @IsNumber() @Min(0) @Max(100) diff --git a/apps/backend/src/modules/escrow/dto/fulfill-condition.dto.ts b/apps/backend/src/modules/escrow/dto/fulfill-condition.dto.ts index fd096ffd..bedd8613 100644 --- a/apps/backend/src/modules/escrow/dto/fulfill-condition.dto.ts +++ b/apps/backend/src/modules/escrow/dto/fulfill-condition.dto.ts @@ -1,11 +1,14 @@ import { IsString, IsOptional, MaxLength } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; export class FulfillConditionDto { + @ApiPropertyOptional({ description: 'Notes explaining the condition fulfillment', example: 'Delivered the first draft' }) @IsString() @IsOptional() @MaxLength(2000) notes?: string; + @ApiPropertyOptional({ description: 'Evidence supporting the fulfillment', example: 'https://example.com/evidence.pdf' }) @IsString() @IsOptional() @MaxLength(500) diff --git a/apps/backend/src/modules/escrow/dto/fund-escrow.dto.ts b/apps/backend/src/modules/escrow/dto/fund-escrow.dto.ts index cd2e6ab7..0be3b1e3 100644 --- a/apps/backend/src/modules/escrow/dto/fund-escrow.dto.ts +++ b/apps/backend/src/modules/escrow/dto/fund-escrow.dto.ts @@ -1,6 +1,8 @@ import { IsNumber, IsPositive } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; export class FundEscrowDto { + @ApiProperty({ description: 'Amount to fund the escrow with', example: 250 }) @IsNumber() @IsPositive() amount: number; diff --git a/apps/backend/src/modules/escrow/dto/list-escrows.dto.ts b/apps/backend/src/modules/escrow/dto/list-escrows.dto.ts index b716a80b..89e560a4 100644 --- a/apps/backend/src/modules/escrow/dto/list-escrows.dto.ts +++ b/apps/backend/src/modules/escrow/dto/list-escrows.dto.ts @@ -1,5 +1,6 @@ import { IsString, IsOptional, IsInt, Min, Max, IsEnum } from 'class-validator'; import { Type } from 'class-transformer'; +import { ApiPropertyOptional } from '@nestjs/swagger'; import { EscrowStatus, EscrowType } from '../entities/escrow.entity'; import { PartyRole } from '../entities/party.entity'; @@ -16,12 +17,14 @@ export enum SortOrder { } export class ListEscrowsDto { + @ApiPropertyOptional({ description: 'Page number for pagination', example: 1 }) @IsInt() @Min(1) @IsOptional() @Type(() => Number) page?: number = 1; + @ApiPropertyOptional({ description: 'Page size for pagination', example: 20 }) @IsInt() @Min(1) @Max(100) @@ -29,34 +32,42 @@ export class ListEscrowsDto { @Type(() => Number) limit?: number = 10; + @ApiPropertyOptional({ enum: EscrowStatus, description: 'Filter escrows by status' }) @IsEnum(EscrowStatus) @IsOptional() status?: EscrowStatus; + @ApiPropertyOptional({ enum: EscrowType, description: 'Filter escrows by type' }) @IsEnum(EscrowType) @IsOptional() type?: EscrowType; + @ApiPropertyOptional({ enum: PartyRole, description: 'Filter escrows by the current user role' }) @IsEnum(PartyRole) @IsOptional() role?: PartyRole; + @ApiPropertyOptional({ enum: SortBy, description: 'Field used to sort escrow results' }) @IsEnum(SortBy) @IsOptional() sortBy?: SortBy = SortBy.CREATED_AT; + @ApiPropertyOptional({ enum: SortOrder, description: 'Sort direction for escrow results' }) @IsEnum(SortOrder) @IsOptional() sortOrder?: SortOrder = SortOrder.DESC; + @ApiPropertyOptional({ description: 'Full-text search across escrow titles and descriptions', example: 'design' }) @IsString() @IsOptional() search?: string; + @ApiPropertyOptional({ description: 'Filter escrows by asset code', example: 'XLM' }) @IsString() @IsOptional() assetCode?: string; + @ApiPropertyOptional({ description: 'Filter escrows by asset issuer', example: 'GA...' }) @IsString() @IsOptional() assetIssuer?: string; diff --git a/apps/backend/src/modules/escrow/dto/list-events.dto.ts b/apps/backend/src/modules/escrow/dto/list-events.dto.ts index 568c6d3a..6de07895 100644 --- a/apps/backend/src/modules/escrow/dto/list-events.dto.ts +++ b/apps/backend/src/modules/escrow/dto/list-events.dto.ts @@ -9,6 +9,7 @@ import { IsUUID, } from 'class-validator'; import { Type } from 'class-transformer'; +import { ApiPropertyOptional } from '@nestjs/swagger'; import { EscrowEventType } from '../entities/escrow-event.entity'; export enum EventSortBy { @@ -23,12 +24,14 @@ export enum EventSortOrder { } export class ListEventsDto { + @ApiPropertyOptional({ description: 'Page number for pagination', example: 1 }) @IsInt() @Min(1) @IsOptional() @Type(() => Number) page?: number = 1; + @ApiPropertyOptional({ description: 'Page size for pagination', example: 20 }) @IsInt() @Min(1) @Max(100) @@ -36,30 +39,37 @@ export class ListEventsDto { @Type(() => Number) limit?: number = 10; + @ApiPropertyOptional({ enum: EscrowEventType, description: 'Filter events by type' }) @IsEnum(EscrowEventType) @IsOptional() eventType?: EscrowEventType; + @ApiPropertyOptional({ description: 'Filter events by actor ID', example: 'user_123' }) @IsString() @IsOptional() actorId?: string; + @ApiPropertyOptional({ description: 'Start date for event filtering', example: '2026-01-01T00:00:00.000Z' }) @IsDateString() @IsOptional() dateFrom?: string; + @ApiPropertyOptional({ description: 'End date for event filtering', example: '2026-01-31T23:59:59.000Z' }) @IsDateString() @IsOptional() dateTo?: string; + @ApiPropertyOptional({ enum: EventSortBy, description: 'Sort field for event results' }) @IsEnum(EventSortBy) @IsOptional() sortBy?: EventSortBy = EventSortBy.CREATED_AT; + @ApiPropertyOptional({ description: 'Filter events for a specific escrow', example: '00000000-0000-0000-0000-000000000000' }) @IsUUID() @IsOptional() escrowId?: string; + @ApiPropertyOptional({ enum: EventSortOrder, description: 'Sort direction for event results' }) @IsEnum(EventSortOrder) @IsOptional() sortOrder?: EventSortOrder = EventSortOrder.DESC; diff --git a/apps/backend/src/modules/escrow/dto/update-escrow.dto.ts b/apps/backend/src/modules/escrow/dto/update-escrow.dto.ts index c171488d..bff6a58a 100644 --- a/apps/backend/src/modules/escrow/dto/update-escrow.dto.ts +++ b/apps/backend/src/modules/escrow/dto/update-escrow.dto.ts @@ -1,16 +1,20 @@ import { IsString, IsOptional, IsDateString, MaxLength } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; export class UpdateEscrowDto { + @ApiPropertyOptional({ description: 'Updated escrow title', example: 'Updated website redesign' }) @IsString() @IsOptional() @MaxLength(255) title?: string; + @ApiPropertyOptional({ description: 'Updated escrow description', example: 'Updated scope for the landing page' }) @IsString() @IsOptional() @MaxLength(2000) description?: string; + @ApiPropertyOptional({ description: 'Updated expiry date for the escrow', example: '2026-12-31T23:59:59.000Z' }) @IsDateString() @IsOptional() expiresAt?: string; diff --git a/apps/backend/src/modules/health/health.controller.ts b/apps/backend/src/modules/health/health.controller.ts index d8324c5b..6a259686 100644 --- a/apps/backend/src/modules/health/health.controller.ts +++ b/apps/backend/src/modules/health/health.controller.ts @@ -1,4 +1,9 @@ -import { Controller, Get } from '@nestjs/common'; +import { Controller, Get, HttpStatus } from '@nestjs/common'; +import { + ApiOperation, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; import { HealthCheck, HealthCheckService, @@ -23,6 +28,7 @@ interface HealthInfo { } @Controller('health') +@ApiTags('health') export class HealthController { constructor( private health: HealthCheckService, @@ -34,6 +40,9 @@ export class HealthController { @Get() @HealthCheck() + @ApiOperation({ summary: 'Perform a health check for the backend service' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Service health status' }) + @ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, description: 'Service health check failed' }) async check(): Promise { return this.health.check([ () => this.checkDatabase(), @@ -43,17 +52,24 @@ export class HealthController { } @Get('live') + @ApiOperation({ summary: 'Check whether the service is live' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Service is alive' }) live(): { status: string } { return { status: 'ok' }; } @Get('ready') @HealthCheck() + @ApiOperation({ summary: 'Check whether the service is ready to serve traffic' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Service is ready' }) + @ApiResponse({ status: HttpStatus.SERVICE_UNAVAILABLE, description: 'Service is not ready' }) async ready(): Promise { return this.health.check([() => this.checkDatabase()]); } @Get('info') + @ApiOperation({ summary: 'Get deployment and runtime information' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Runtime information retrieved successfully' }) async info(): Promise { const activeEscrows = await this.escrowRepository.count({ where: { status: EscrowStatus.ACTIVE }, diff --git a/apps/backend/src/modules/ipfs/ipfs.controller.ts b/apps/backend/src/modules/ipfs/ipfs.controller.ts index bfba4066..3e7eb982 100644 --- a/apps/backend/src/modules/ipfs/ipfs.controller.ts +++ b/apps/backend/src/modules/ipfs/ipfs.controller.ts @@ -1,12 +1,17 @@ -import { Controller, Get, Param, Redirect } from '@nestjs/common'; +import { Controller, Get, Param, Redirect, HttpStatus } from '@nestjs/common'; +import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import { IpfsService } from './ipfs.service'; @Controller('ipfs') +@ApiTags('ipfs') export class IpfsController { constructor(private readonly ipfsService: IpfsService) {} @Get(':cid') @Redirect() + @ApiOperation({ summary: 'Redirect an IPFS content identifier to the configured gateway' }) + @ApiParam({ name: 'cid', description: 'IPFS content identifier' }) + @ApiResponse({ status: HttpStatus.FOUND, description: 'Content redirected to the IPFS gateway' }) getFile(@Param('cid') cid: string) { const url = this.ipfsService.getGatewayUrl(cid); return { url, statusCode: 302 }; diff --git a/apps/backend/src/modules/stellar/controllers/stellar-event.controller.ts b/apps/backend/src/modules/stellar/controllers/stellar-event.controller.ts index b301b19c..9354745e 100644 --- a/apps/backend/src/modules/stellar/controllers/stellar-event.controller.ts +++ b/apps/backend/src/modules/stellar/controllers/stellar-event.controller.ts @@ -6,21 +6,33 @@ import { HttpCode, HttpStatus, } from '@nestjs/common'; +import { + ApiOperation, + ApiQuery, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; import { StellarEventListenerService } from '../services/stellar-event-listener.service'; @Controller('stellar/events') +@ApiTags('stellar/events') export class StellarEventController { constructor( private readonly stellarEventListenerService: StellarEventListenerService, ) {} @Get('status') + @ApiOperation({ summary: 'Get the current Stellar event sync status' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Sync status retrieved successfully' }) getSyncStatus() { return this.stellarEventListenerService.getSyncStatus(); } @Post('sync') @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Trigger a Stellar ledger sync' }) + @ApiQuery({ name: 'ledger', required: false, description: 'Starting ledger number' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Sync started successfully' }) async syncFromLedger(@Query('ledger') ledger?: string) { const startLedger = ledger ? parseInt(ledger, 10) : undefined; await this.stellarEventListenerService.syncFromLedger(startLedger || 0); @@ -29,6 +41,8 @@ export class StellarEventController { @Post('restart') @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Restart the Stellar event listener' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Listener restarted successfully' }) async restartListener() { await this.stellarEventListenerService.stopEventListener(); await this.stellarEventListenerService.startEventListener(); diff --git a/apps/backend/src/modules/webhook/webhook.controller.ts b/apps/backend/src/modules/webhook/webhook.controller.ts index fa11b0ff..c67df493 100644 --- a/apps/backend/src/modules/webhook/webhook.controller.ts +++ b/apps/backend/src/modules/webhook/webhook.controller.ts @@ -8,7 +8,16 @@ import { Req, UseGuards, UseInterceptors, + HttpStatus, } from '@nestjs/common'; +import { + ApiBearerAuth, + ApiBody, + ApiOperation, + ApiParam, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; import { WebhookService } from '../../services/webhook/webhook.service'; import { WebhookEvent } from '../../types/webhook/webhook.types'; import { AuthGuard } from '../auth/middleware/auth.guard'; @@ -21,12 +30,19 @@ class CreateWebhookDto { } @Controller('webhooks') +@ApiTags('webhooks') +@ApiBearerAuth('access-token') @UseGuards(AuthGuard) export class WebhookController { constructor(private readonly webhookService: WebhookService) {} @Post() @UseInterceptors(ThrottlerGuard) + @ApiOperation({ summary: 'Create a webhook subscription for the authenticated user' }) + @ApiBody({ description: 'Webhook registration payload', type: CreateWebhookDto }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Webhook created successfully' }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid webhook payload' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) async create( @Req() req: { user: { id: string } }, @Body() dto: CreateWebhookDto, @@ -42,6 +58,9 @@ export class WebhookController { } @Get() + @ApiOperation({ summary: 'List webhooks owned by the authenticated user' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Webhooks retrieved successfully' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) async list(@Req() req: { user: { id: string } }) { const userId = req?.user?.id; if (!userId) throw new Error('User ID missing'); @@ -49,6 +68,11 @@ export class WebhookController { } @Delete(':id') + @ApiOperation({ summary: 'Delete a webhook by ID' }) + @ApiParam({ name: 'id', description: 'Webhook ID to delete' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Webhook deleted successfully' }) + @ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'Webhook not found' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) async remove(@Req() req: { user: { id: string } }, @Param('id') id: string) { const userId = req?.user?.id; if (!userId) throw new Error('User ID missing'); diff --git a/apps/backend/src/notifications/entities/update-preferences.dto.ts b/apps/backend/src/notifications/entities/update-preferences.dto.ts index bac19087..87f85f0d 100644 --- a/apps/backend/src/notifications/entities/update-preferences.dto.ts +++ b/apps/backend/src/notifications/entities/update-preferences.dto.ts @@ -1,14 +1,18 @@ import { IsEnum, IsBoolean, IsArray, ArrayNotEmpty } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; import { NotificationChannel } from '../enums/notification-event.enum'; import { NotificationEventType } from '../enums/notification-event.enum'; export class UpdatePreferencesDto { + @ApiProperty({ enum: NotificationChannel, description: 'Notification channel to update' }) @IsEnum(NotificationChannel) channel: NotificationChannel; + @ApiProperty({ description: 'Whether notifications are enabled for this channel', example: true }) @IsBoolean() enabled: boolean; + @ApiProperty({ description: 'Event types to enable or disable', example: ['escrow_created'] }) @IsArray() @ArrayNotEmpty() @IsEnum(NotificationEventType, { each: true }) diff --git a/apps/backend/src/notifications/notifications.controller.ts b/apps/backend/src/notifications/notifications.controller.ts index dccc0d50..b48b5575 100644 --- a/apps/backend/src/notifications/notifications.controller.ts +++ b/apps/backend/src/notifications/notifications.controller.ts @@ -9,7 +9,15 @@ import { Req, UseGuards, Post, + HttpStatus, } from '@nestjs/common'; +import { + ApiBearerAuth, + ApiBody, + ApiOperation, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; import { Request } from 'express'; import { UpdatePreferencesDto } from './entities/update-preferences.dto'; @@ -20,6 +28,8 @@ interface AuthenticatedRequest extends Request { } @Controller('notifications') +@ApiTags('notifications') +@ApiBearerAuth('access-token') @UseGuards(AuthGuard) export class NotificationController { constructor( @@ -28,11 +38,19 @@ export class NotificationController { ) {} @Get('preferences') + @ApiOperation({ summary: 'Get notification preferences for the authenticated user' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Preferences retrieved successfully' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) getPreferences(@Req() req: AuthenticatedRequest) { return this.preferenceService.getUserPreferences(req.user.id); } @Put('preferences') + @ApiOperation({ summary: 'Update notification preferences for the authenticated user' }) + @ApiBody({ type: [UpdatePreferencesDto], description: 'Notification preference updates' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Preferences updated successfully' }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid preferences payload' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) updatePreferences( @Req() req: AuthenticatedRequest, @Body() dto: UpdatePreferencesDto[], @@ -41,16 +59,27 @@ export class NotificationController { } @Get() + @ApiOperation({ summary: 'List notifications for the authenticated user' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Notifications retrieved successfully' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) getNotifications(@Req() req: AuthenticatedRequest) { return this.notificationService.getUserNotifications(req.user.id); } @Get('unread-count') + @ApiOperation({ summary: 'Get the unread notification count for the authenticated user' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Unread count retrieved successfully' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) getUnreadCount(@Req() req: AuthenticatedRequest) { return this.notificationService.getUnreadCount(req.user.id); } @Post('mark-as-read') + @ApiOperation({ summary: 'Mark one or more notifications as read' }) + @ApiBody({ description: 'Notification ID to mark as read', schema: { type: 'object', properties: { notificationId: { type: 'string' } } } }) + @ApiResponse({ status: HttpStatus.OK, description: 'Notifications marked as read' }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid request payload' }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Authentication required' }) async markAsRead( @Req() req: AuthenticatedRequest, @Body('notificationId') notificationId?: string, diff --git a/apps/backend/src/swagger-docs.spec.ts b/apps/backend/src/swagger-docs.spec.ts new file mode 100644 index 00000000..c5d5369a --- /dev/null +++ b/apps/backend/src/swagger-docs.spec.ts @@ -0,0 +1,43 @@ +import { readFileSync } from 'fs'; +import { join } from 'path'; + +describe('Swagger documentation coverage', () => { + const read = (relativePath: string) => + readFileSync(join(__dirname, relativePath), 'utf8'); + + it('documents the core backend controllers and DTOs', () => { + const controllerFiles = [ + ['modules/escrow/controllers/escrow.controller.ts', '@ApiTags('], + ['modules/auth/controllers/auth.controller.ts', '@ApiTags('], + ['modules/admin/admin.controller.ts', '@ApiTags('], + ['modules/assets/assets.controller.ts', '@ApiTags('], + ['modules/health/health.controller.ts', '@ApiTags('], + ['modules/webhook/webhook.controller.ts', '@ApiTags('], + ['notifications/notifications.controller.ts', '@ApiTags('], + ]; + + for (const [relativePath, expectedSnippet] of controllerFiles) { + const content = read(relativePath); + expect(content).toContain(expectedSnippet); + expect(content).toContain('@ApiOperation('); + } + + const dtoFiles = [ + 'modules/escrow/dto/create-escrow.dto.ts', + 'modules/escrow/dto/update-escrow.dto.ts', + 'modules/auth/dto/auth.dto.ts', + 'modules/auth/dto/profile.dto.ts', + 'notifications/entities/update-preferences.dto.ts', + 'modules/assets/dto/asset.dto.ts', + ]; + + for (const relativePath of dtoFiles) { + const content = read(relativePath); + expect(content).toContain('@ApiProperty'); + } + + const mainContent = read('main.ts'); + expect(mainContent).toContain("SwaggerModule.setup('api/docs'"); + expect(mainContent).toContain('.addBearerAuth('); + }); +});