From 28f7241bcdf898a7d184e8dd7108aafecf9d4a2d Mon Sep 17 00:00:00 2001 From: boutmoun123 Date: Mon, 20 Apr 2026 15:12:16 +0300 Subject: [PATCH] first commit --- .env.example | 44 + .gitignore | 12 + .prettierrc | 6 + README.md | 3 + jest.config.js | 11 + nest-cli.json | 8 + package-lock.json | 10872 ++++++++++++++++ package.json | 67 + ...a-Auth-Users-Posts.postman_collection.json | 2745 ++++ .../Oudelaa-Local.postman_environment.json | 39 + postman/superadmin-dashboard-config.json | 209 + src/app.controller.ts | 12 + src/app.module.ts | 58 + src/app.service.ts | 8 + .../decorators/current-user.decorator.ts | 6 + src/common/decorators/public.decorator.ts | 4 + src/common/decorators/roles.decorator.ts | 5 + src/common/decorators/throttle.decorator.ts | 11 + src/common/dto/object-id-param.dto.ts | 6 + src/common/dto/pagination-query.dto.ts | 22 + src/common/enums/experience-level.enum.ts | 6 + src/common/enums/music-role.enum.ts | 8 + src/common/enums/notification-type.enum.ts | 6 + src/common/enums/post-type.enum.ts | 5 + src/common/enums/post-visibility.enum.ts | 5 + src/common/enums/token-type.enum.ts | 4 + src/common/enums/user-role.enum.ts | 5 + src/common/guards/jwt-auth.guard.ts | 5 + src/common/guards/jwt-refresh.guard.ts | 5 + src/common/guards/roles.guard.ts | 29 + .../guards/super-admin-jwt-auth.guard.ts | 5 + src/common/guards/throttle.guard.ts | 50 + .../response-envelope.interceptor.ts | 25 + .../interfaces/jwt-payload.interface.ts | 7 + src/common/utils/cursor.util.spec.ts | 13 + src/common/utils/cursor.util.ts | 19 + src/common/utils/hash.util.ts | 7 + src/config/configuration.ts | 69 + src/config/constants.ts | 5 + src/config/swagger.config.ts | 17 + src/config/validation.schema.ts | 44 + src/database/database.module.ts | 16 + src/database/mongoose-options.factory.ts | 7 + src/main.ts | 84 + src/modules/audit/audit.module.ts | 19 + src/modules/audit/audit.repository.ts | 29 + src/modules/audit/audit.service.ts | 24 + src/modules/audit/schemas/audit-log.schema.ts | 31 + src/modules/auth/auth.controller.ts | 154 + src/modules/auth/auth.module.ts | 73 + src/modules/auth/auth.repository.ts | 237 + src/modules/auth/auth.service.ts | 666 + src/modules/auth/dto/auth-response.dto.ts | 16 + src/modules/auth/dto/forgot-password.dto.ts | 8 + .../auth/dto/google-token-login.dto.ts | 9 + src/modules/auth/dto/login.dto.ts | 13 + src/modules/auth/dto/refresh-token.dto.ts | 9 + src/modules/auth/dto/register-basic.dto.ts | 18 + src/modules/auth/dto/register.dto.ts | 112 + src/modules/auth/dto/reset-password.dto.ts | 18 + .../auth/dto/send-email-verification.dto.ts | 8 + src/modules/auth/dto/super-admin-login.dto.ts | 13 + src/modules/auth/dto/verify-email.dto.ts | 14 + src/modules/auth/dto/verify-reset-code.dto.ts | 14 + src/modules/auth/guards/google-auth.guard.ts | 5 + .../schemas/email-verification-code.schema.ts | 28 + .../schemas/password-reset-code.schema.ts | 31 + .../auth/schemas/refresh-token.schema.ts | 32 + .../super-admin-refresh-token.schema.ts | 23 + .../auth/strategies/google.strategy.ts | 42 + .../auth/strategies/jwt-refresh.strategy.ts | 26 + src/modules/auth/strategies/jwt.strategy.ts | 24 + .../strategies/super-admin-jwt.strategy.ts | 24 + src/modules/auth/types/token-pair.type.ts | 8 + src/modules/chat/chat.controller.ts | 78 + src/modules/chat/chat.gateway.ts | 137 + src/modules/chat/chat.module.ts | 29 + src/modules/chat/chat.repository.ts | 221 + src/modules/chat/chat.service.ts | 250 + .../chat/dto/create-conversation.dto.ts | 19 + src/modules/chat/dto/message-query.dto.ts | 3 + src/modules/chat/dto/send-message.dto.ts | 23 + src/modules/chat/schemas/chat-block.schema.ts | 17 + .../chat/schemas/conversation.schema.ts | 37 + src/modules/chat/schemas/message.schema.ts | 33 + src/modules/comments/comments.controller.ts | 50 + src/modules/comments/comments.module.ts | 20 + src/modules/comments/comments.repository.ts | 77 + src/modules/comments/comments.service.ts | 114 + src/modules/comments/dto/comment-query.dto.ts | 3 + .../comments/dto/create-comment.dto.ts | 18 + .../comments/dto/update-comment.dto.ts | 8 + .../comments/schemas/comment.schema.ts | 34 + src/modules/email/email.module.ts | 8 + src/modules/email/email.service.ts | 134 + src/modules/feed/dto/feed-query.dto.ts | 22 + src/modules/feed/feed.controller.ts | 27 + src/modules/feed/feed.module.ts | 22 + src/modules/feed/feed.repository.ts | 58 + src/modules/feed/feed.service.ts | 212 + src/modules/follows/dto/toggle-follow.dto.ts | 6 + src/modules/follows/follows.controller.ts | 52 + src/modules/follows/follows.module.ts | 25 + src/modules/follows/follows.repository.ts | 65 + src/modules/follows/follows.service.spec.ts | 42 + src/modules/follows/follows.service.ts | 223 + src/modules/follows/schemas/follow.schema.ts | 17 + src/modules/likes/dto/toggle-like.dto.ts | 12 + src/modules/likes/likes.controller.ts | 43 + src/modules/likes/likes.module.ts | 20 + src/modules/likes/likes.repository.ts | 31 + src/modules/likes/likes.service.spec.ts | 30 + src/modules/likes/likes.service.ts | 78 + src/modules/likes/schemas/like.schema.ts | 19 + .../marketplace/dto/create-instrument.dto.ts | 50 + .../marketplace/dto/instrument-query.dto.ts | 33 + .../marketplace/dto/update-instrument.dto.ts | 4 + .../marketplace/marketplace.controller.ts | 69 + src/modules/marketplace/marketplace.module.ts | 18 + .../marketplace/marketplace.repository.ts | 135 + .../marketplace/marketplace.service.ts | 168 + .../marketplace/schemas/instrument.schema.ts | 37 + src/modules/media/dto/upload-media.dto.ts | 7 + src/modules/media/media.controller.ts | 6 + src/modules/media/media.module.ts | 11 + src/modules/media/media.repository.ts | 4 + src/modules/media/media.service.ts | 4 + .../media/schemas/media-file.schema.ts | 22 + .../dto/create-notification.dto.ts | 16 + .../dto/notification-query.dto.ts | 14 + .../notifications/notifications.controller.ts | 35 + .../notifications/notifications.gateway.ts | 71 + .../notifications/notifications.module.ts | 21 + .../notifications/notifications.repository.ts | 100 + .../notifications.service.spec.ts | 44 + .../notifications/notifications.service.ts | 107 + .../schemas/notification.schema.ts | 31 + src/modules/outbox/outbox.module.ts | 15 + src/modules/outbox/outbox.service.ts | 57 + .../outbox/schemas/outbox-event.schema.ts | 28 + src/modules/posts/dto/create-post.dto.ts | 25 + src/modules/posts/dto/post-query.dto.ts | 11 + src/modules/posts/dto/update-post.dto.ts | 26 + src/modules/posts/posts.controller.ts | 55 + src/modules/posts/posts.module.ts | 23 + src/modules/posts/posts.repository.ts | 121 + src/modules/posts/posts.service.ts | 155 + src/modules/posts/schemas/post.schema.ts | 59 + src/modules/saves/dto/toggle-save.dto.ts | 6 + src/modules/saves/saves.controller.ts | 41 + src/modules/saves/saves.module.ts | 18 + src/modules/saves/saves.repository.ts | 42 + src/modules/saves/saves.service.spec.ts | 25 + src/modules/saves/saves.service.ts | 87 + src/modules/saves/schemas/save.schema.ts | 17 + .../users/dto/admin-disable-user.dto.ts | 10 + src/modules/users/dto/create-admin.dto.ts | 27 + src/modules/users/dto/create-user.dto.ts | 115 + src/modules/users/dto/music-setup.dto.ts | 35 + src/modules/users/dto/profile-setup.dto.ts | 44 + src/modules/users/dto/update-user-role.dto.ts | 7 + src/modules/users/dto/update-user.dto.ts | 69 + src/modules/users/dto/user-query.dto.ts | 15 + src/modules/users/schemas/user.schema.ts | 124 + src/modules/users/users.controller.ts | 188 + src/modules/users/users.module.ts | 23 + src/modules/users/users.repository.ts | 90 + src/modules/users/users.service.ts | 649 + test/app.e2e-spec.ts | 21 + test/jest-e2e.json | 9 + tsconfig.build.json | 4 + tsconfig.json | 20 + 172 files changed, 21907 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 README.md create mode 100644 jest.config.js create mode 100644 nest-cli.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postman/Oudelaa-Auth-Users-Posts.postman_collection.json create mode 100644 postman/Oudelaa-Local.postman_environment.json create mode 100644 postman/superadmin-dashboard-config.json create mode 100644 src/app.controller.ts create mode 100644 src/app.module.ts create mode 100644 src/app.service.ts create mode 100644 src/common/decorators/current-user.decorator.ts create mode 100644 src/common/decorators/public.decorator.ts create mode 100644 src/common/decorators/roles.decorator.ts create mode 100644 src/common/decorators/throttle.decorator.ts create mode 100644 src/common/dto/object-id-param.dto.ts create mode 100644 src/common/dto/pagination-query.dto.ts create mode 100644 src/common/enums/experience-level.enum.ts create mode 100644 src/common/enums/music-role.enum.ts create mode 100644 src/common/enums/notification-type.enum.ts create mode 100644 src/common/enums/post-type.enum.ts create mode 100644 src/common/enums/post-visibility.enum.ts create mode 100644 src/common/enums/token-type.enum.ts create mode 100644 src/common/enums/user-role.enum.ts create mode 100644 src/common/guards/jwt-auth.guard.ts create mode 100644 src/common/guards/jwt-refresh.guard.ts create mode 100644 src/common/guards/roles.guard.ts create mode 100644 src/common/guards/super-admin-jwt-auth.guard.ts create mode 100644 src/common/guards/throttle.guard.ts create mode 100644 src/common/interceptors/response-envelope.interceptor.ts create mode 100644 src/common/interfaces/jwt-payload.interface.ts create mode 100644 src/common/utils/cursor.util.spec.ts create mode 100644 src/common/utils/cursor.util.ts create mode 100644 src/common/utils/hash.util.ts create mode 100644 src/config/configuration.ts create mode 100644 src/config/constants.ts create mode 100644 src/config/swagger.config.ts create mode 100644 src/config/validation.schema.ts create mode 100644 src/database/database.module.ts create mode 100644 src/database/mongoose-options.factory.ts create mode 100644 src/main.ts create mode 100644 src/modules/audit/audit.module.ts create mode 100644 src/modules/audit/audit.repository.ts create mode 100644 src/modules/audit/audit.service.ts create mode 100644 src/modules/audit/schemas/audit-log.schema.ts create mode 100644 src/modules/auth/auth.controller.ts create mode 100644 src/modules/auth/auth.module.ts create mode 100644 src/modules/auth/auth.repository.ts create mode 100644 src/modules/auth/auth.service.ts create mode 100644 src/modules/auth/dto/auth-response.dto.ts create mode 100644 src/modules/auth/dto/forgot-password.dto.ts create mode 100644 src/modules/auth/dto/google-token-login.dto.ts create mode 100644 src/modules/auth/dto/login.dto.ts create mode 100644 src/modules/auth/dto/refresh-token.dto.ts create mode 100644 src/modules/auth/dto/register-basic.dto.ts create mode 100644 src/modules/auth/dto/register.dto.ts create mode 100644 src/modules/auth/dto/reset-password.dto.ts create mode 100644 src/modules/auth/dto/send-email-verification.dto.ts create mode 100644 src/modules/auth/dto/super-admin-login.dto.ts create mode 100644 src/modules/auth/dto/verify-email.dto.ts create mode 100644 src/modules/auth/dto/verify-reset-code.dto.ts create mode 100644 src/modules/auth/guards/google-auth.guard.ts create mode 100644 src/modules/auth/schemas/email-verification-code.schema.ts create mode 100644 src/modules/auth/schemas/password-reset-code.schema.ts create mode 100644 src/modules/auth/schemas/refresh-token.schema.ts create mode 100644 src/modules/auth/schemas/super-admin-refresh-token.schema.ts create mode 100644 src/modules/auth/strategies/google.strategy.ts create mode 100644 src/modules/auth/strategies/jwt-refresh.strategy.ts create mode 100644 src/modules/auth/strategies/jwt.strategy.ts create mode 100644 src/modules/auth/strategies/super-admin-jwt.strategy.ts create mode 100644 src/modules/auth/types/token-pair.type.ts create mode 100644 src/modules/chat/chat.controller.ts create mode 100644 src/modules/chat/chat.gateway.ts create mode 100644 src/modules/chat/chat.module.ts create mode 100644 src/modules/chat/chat.repository.ts create mode 100644 src/modules/chat/chat.service.ts create mode 100644 src/modules/chat/dto/create-conversation.dto.ts create mode 100644 src/modules/chat/dto/message-query.dto.ts create mode 100644 src/modules/chat/dto/send-message.dto.ts create mode 100644 src/modules/chat/schemas/chat-block.schema.ts create mode 100644 src/modules/chat/schemas/conversation.schema.ts create mode 100644 src/modules/chat/schemas/message.schema.ts create mode 100644 src/modules/comments/comments.controller.ts create mode 100644 src/modules/comments/comments.module.ts create mode 100644 src/modules/comments/comments.repository.ts create mode 100644 src/modules/comments/comments.service.ts create mode 100644 src/modules/comments/dto/comment-query.dto.ts create mode 100644 src/modules/comments/dto/create-comment.dto.ts create mode 100644 src/modules/comments/dto/update-comment.dto.ts create mode 100644 src/modules/comments/schemas/comment.schema.ts create mode 100644 src/modules/email/email.module.ts create mode 100644 src/modules/email/email.service.ts create mode 100644 src/modules/feed/dto/feed-query.dto.ts create mode 100644 src/modules/feed/feed.controller.ts create mode 100644 src/modules/feed/feed.module.ts create mode 100644 src/modules/feed/feed.repository.ts create mode 100644 src/modules/feed/feed.service.ts create mode 100644 src/modules/follows/dto/toggle-follow.dto.ts create mode 100644 src/modules/follows/follows.controller.ts create mode 100644 src/modules/follows/follows.module.ts create mode 100644 src/modules/follows/follows.repository.ts create mode 100644 src/modules/follows/follows.service.spec.ts create mode 100644 src/modules/follows/follows.service.ts create mode 100644 src/modules/follows/schemas/follow.schema.ts create mode 100644 src/modules/likes/dto/toggle-like.dto.ts create mode 100644 src/modules/likes/likes.controller.ts create mode 100644 src/modules/likes/likes.module.ts create mode 100644 src/modules/likes/likes.repository.ts create mode 100644 src/modules/likes/likes.service.spec.ts create mode 100644 src/modules/likes/likes.service.ts create mode 100644 src/modules/likes/schemas/like.schema.ts create mode 100644 src/modules/marketplace/dto/create-instrument.dto.ts create mode 100644 src/modules/marketplace/dto/instrument-query.dto.ts create mode 100644 src/modules/marketplace/dto/update-instrument.dto.ts create mode 100644 src/modules/marketplace/marketplace.controller.ts create mode 100644 src/modules/marketplace/marketplace.module.ts create mode 100644 src/modules/marketplace/marketplace.repository.ts create mode 100644 src/modules/marketplace/marketplace.service.ts create mode 100644 src/modules/marketplace/schemas/instrument.schema.ts create mode 100644 src/modules/media/dto/upload-media.dto.ts create mode 100644 src/modules/media/media.controller.ts create mode 100644 src/modules/media/media.module.ts create mode 100644 src/modules/media/media.repository.ts create mode 100644 src/modules/media/media.service.ts create mode 100644 src/modules/media/schemas/media-file.schema.ts create mode 100644 src/modules/notifications/dto/create-notification.dto.ts create mode 100644 src/modules/notifications/dto/notification-query.dto.ts create mode 100644 src/modules/notifications/notifications.controller.ts create mode 100644 src/modules/notifications/notifications.gateway.ts create mode 100644 src/modules/notifications/notifications.module.ts create mode 100644 src/modules/notifications/notifications.repository.ts create mode 100644 src/modules/notifications/notifications.service.spec.ts create mode 100644 src/modules/notifications/notifications.service.ts create mode 100644 src/modules/notifications/schemas/notification.schema.ts create mode 100644 src/modules/outbox/outbox.module.ts create mode 100644 src/modules/outbox/outbox.service.ts create mode 100644 src/modules/outbox/schemas/outbox-event.schema.ts create mode 100644 src/modules/posts/dto/create-post.dto.ts create mode 100644 src/modules/posts/dto/post-query.dto.ts create mode 100644 src/modules/posts/dto/update-post.dto.ts create mode 100644 src/modules/posts/posts.controller.ts create mode 100644 src/modules/posts/posts.module.ts create mode 100644 src/modules/posts/posts.repository.ts create mode 100644 src/modules/posts/posts.service.ts create mode 100644 src/modules/posts/schemas/post.schema.ts create mode 100644 src/modules/saves/dto/toggle-save.dto.ts create mode 100644 src/modules/saves/saves.controller.ts create mode 100644 src/modules/saves/saves.module.ts create mode 100644 src/modules/saves/saves.repository.ts create mode 100644 src/modules/saves/saves.service.spec.ts create mode 100644 src/modules/saves/saves.service.ts create mode 100644 src/modules/saves/schemas/save.schema.ts create mode 100644 src/modules/users/dto/admin-disable-user.dto.ts create mode 100644 src/modules/users/dto/create-admin.dto.ts create mode 100644 src/modules/users/dto/create-user.dto.ts create mode 100644 src/modules/users/dto/music-setup.dto.ts create mode 100644 src/modules/users/dto/profile-setup.dto.ts create mode 100644 src/modules/users/dto/update-user-role.dto.ts create mode 100644 src/modules/users/dto/update-user.dto.ts create mode 100644 src/modules/users/dto/user-query.dto.ts create mode 100644 src/modules/users/schemas/user.schema.ts create mode 100644 src/modules/users/users.controller.ts create mode 100644 src/modules/users/users.module.ts create mode 100644 src/modules/users/users.repository.ts create mode 100644 src/modules/users/users.service.ts create mode 100644 test/app.e2e-spec.ts create mode 100644 test/jest-e2e.json create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d571624 --- /dev/null +++ b/.env.example @@ -0,0 +1,44 @@ +NODE_ENV=development +PORT=4000 +HOST=0.0.0.0 +PUBLIC_BASE_URL=http://localhost:4000 +RESPONSE_ENVELOPE_ENABLED=false +GLOBAL_PREFIX=api/v1 +CORS_ORIGINS=http://localhost:3000,http://192.168.1.14:3000,http://192.168.1.14:5173 +MONGODB_URI=mongodb://127.0.0.1:27017/oudelaa +JWT_ACCESS_SECRET=change_me_access_secret +JWT_ACCESS_EXPIRES_IN=15m +JWT_REFRESH_SECRET=change_me_refresh_secret +JWT_REFRESH_EXPIRES_IN=30d +BCRYPT_SALT_ROUNDS=12 +PASSWORD_RESET_CODE_EXPIRES_MINUTES=10 +PASSWORD_RESET_MAX_ATTEMPTS=5 +PASSWORD_RESET_TOKEN_SECRET= +PASSWORD_RESET_TOKEN_EXPIRES_IN=15m +EMAIL_VERIFICATION_CODE_EXPIRES_MINUTES=10 +EMAIL_VERIFICATION_MAX_ATTEMPTS=5 +SWAGGER_TITLE=Oudelaa API +SWAGGER_DESCRIPTION=Social media backend API documentation +SWAGGER_VERSION=1.0.0 +SWAGGER_PATH=docs + +GOOGLE_CLIENT_ID=your_google_client_id +GOOGLE_CLIENT_SECRET=your_google_client_secret +GOOGLE_CALLBACK_URL=http://192.168.1.14:4000/api/v1/auth/google/callback +EMAIL_ENABLED=false +EMAIL_SMTP_HOST=smtp.gmail.com +EMAIL_SMTP_PORT=587 +EMAIL_SMTP_SECURE=false +EMAIL_SMTP_USER= +EMAIL_SMTP_PASS= +EMAIL_FROM_NAME=Oudelaa +EMAIL_FROM_EMAIL= + + +SUPERADMIN_EMAIL=admin@oudelaa.com +SUPERADMIN_PASSWORD=SuperAdminStrongPass123! +SUPERADMIN_ACCESS_SECRET=change_me_superadmin_access_secret +SUPERADMIN_ACCESS_EXPIRES_IN=15m +SUPERADMIN_REFRESH_SECRET=change_me_superadmin_refresh_secret +SUPERADMIN_REFRESH_EXPIRES_IN=30d + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d3f5c1d --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Build +node_modules +dist +uploads + +# Env files +.env + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..e5ce635 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100, + "semi": true +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..825b7cc --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Oudelaa Backend + +Production-oriented NestJS backend for a social media platform. diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..04fb757 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,11 @@ +module.exports = { + moduleFileExtensions: ['js', 'json', 'ts'], + rootDir: '.', + testEnvironment: 'node', + testRegex: '.*\\.spec\\.ts$', + transform: { + '^.+\\.(t|j)s$': 'ts-jest', + }, + collectCoverageFrom: ['src/**/*.(t|j)s'], + coverageDirectory: './coverage', +}; diff --git a/nest-cli.json b/nest-cli.json new file mode 100644 index 0000000..f9aa683 --- /dev/null +++ b/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..93ed528 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,10872 @@ +{ + "name": "oudelaa-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "oudelaa-backend", + "version": "1.0.0", + "license": "UNLICENSED", + "dependencies": { + "@nestjs/common": "^10.4.0", + "@nestjs/config": "^3.2.3", + "@nestjs/core": "^10.4.0", + "@nestjs/jwt": "^10.2.0", + "@nestjs/mongoose": "^10.1.0", + "@nestjs/passport": "^10.0.3", + "@nestjs/platform-express": "^10.4.0", + "@nestjs/platform-socket.io": "^10.4.0", + "@nestjs/swagger": "^8.1.0", + "@nestjs/websockets": "^10.4.0", + "@types/passport-google-oauth20": "^2.0.17", + "bcrypt": "^5.1.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "google-auth-library": "^10.6.2", + "joi": "^17.13.3", + "mongoose": "^8.6.0", + "nodemailer": "^8.0.5", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", + "passport-jwt": "^4.0.1", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "socket.io": "^4.8.0" + }, + "devDependencies": { + "@nestjs/cli": "^10.4.5", + "@nestjs/schematics": "^10.2.3", + "@nestjs/testing": "^10.4.0", + "@types/bcrypt": "^5.0.2", + "@types/express": "^4.17.21", + "@types/jest": "^29.5.12", + "@types/node": "^20.16.5", + "@types/nodemailer": "^8.0.0", + "@types/passport-jwt": "^4.0.1", + "@types/supertest": "^6.0.2", + "eslint": "^9.11.1", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "jest": "^29.7.0", + "prettier": "^3.3.3", + "source-map-support": "^0.5.21", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-loader": "^9.5.1", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.6.2" + } + }, + "node_modules/@angular-devkit/core": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz", + "integrity": "sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.11.tgz", + "integrity": "sha512-I5wviiIqiFwar9Pdk30Lujk8FczEEc18i22A5c6Z9lbmhPQdTroDnEQdsfXjy404wPe8H62s0I15o4pmMGfTYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "jsonc-parser": "3.2.1", + "magic-string": "0.30.8", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-17.3.11.tgz", + "integrity": "sha512-kcOMqp+PHAKkqRad7Zd7PbpqJ0LqLaNZdY1+k66lLWmkEBozgq8v4ASn/puPWf9Bo0HpCiK+EzLf0VHE8Z/y6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "ansi-colors": "4.1.3", + "inquirer": "9.2.15", + "symbol-observable": "4.0.0", + "yargs-parser": "21.1.1" + }, + "bin": { + "schematics": "bin/schematics.js" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/inquirer": { + "version": "9.2.15", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.15.tgz", + "integrity": "sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ljharb/through": "^2.3.12", + "ansi-escapes": "^4.3.2", + "chalk": "^5.3.0", + "cli-cursor": "^3.1.0", + "cli-width": "^4.1.0", + "external-editor": "^3.1.0", + "figures": "^3.2.0", + "lodash": "^4.17.21", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@ljharb/through": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.14.tgz", + "integrity": "sha512-ajBvlKpWucBB17FuQYUShqpqy8GRgYEpJW0vWJbUu1CV9lWyrDCapy0lScU8T8Z6qn49sSwJB3+M+evYIdGg+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", + "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", + "license": "MIT" + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz", + "integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@nestjs/cli": { + "version": "10.4.9", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.9.tgz", + "integrity": "sha512-s8qYd97bggqeK7Op3iD49X2MpFtW4LVNLAwXFkfbRxKME6IYT7X0muNTJ2+QfI8hpbNx9isWkrLWIp+g5FOhiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "@angular-devkit/schematics-cli": "17.3.11", + "@nestjs/schematics": "^10.0.1", + "chalk": "4.1.2", + "chokidar": "3.6.0", + "cli-table3": "0.6.5", + "commander": "4.1.1", + "fork-ts-checker-webpack-plugin": "9.0.2", + "glob": "10.4.5", + "inquirer": "8.2.6", + "node-emoji": "1.11.0", + "ora": "5.4.1", + "tree-kill": "1.2.2", + "tsconfig-paths": "4.2.0", + "tsconfig-paths-webpack-plugin": "4.2.0", + "typescript": "5.7.2", + "webpack": "5.97.1", + "webpack-node-externals": "3.0.0" + }, + "bin": { + "nest": "bin/nest.js" + }, + "engines": { + "node": ">= 16.14" + }, + "peerDependencies": { + "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0", + "@swc/core": "^1.3.62" + }, + "peerDependenciesMeta": { + "@swc/cli": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nestjs/cli/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@nestjs/cli/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@nestjs/cli/node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@nestjs/cli/node_modules/webpack": { + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/@nestjs/common": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.22.tgz", + "integrity": "sha512-fxJ4v85nDHaqT1PmfNCQ37b/jcv2OojtXTaK1P2uAXhzLf9qq6WNUOFvxBrV4fhQek1EQoT1o9oj5xAZmv3NRw==", + "license": "MIT", + "peer": true, + "dependencies": { + "file-type": "20.4.1", + "iterare": "1.2.1", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/config": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.3.0.tgz", + "integrity": "sha512-pdGTp8m9d0ZCrjTpjkUbZx6gyf2IKf+7zlkrPNMsJzYZ4bFRRTpXrnj+556/5uiI6AfL5mMrJc2u7dB6bvM+VA==", + "license": "MIT", + "dependencies": { + "dotenv": "16.4.5", + "dotenv-expand": "10.0.0", + "lodash": "4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/core": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.22.tgz", + "integrity": "sha512-6IX9+VwjiKtCjx+mXVPncpkQ5ZjKfmssOZPFexmT+6T9H9wZ3svpYACAo7+9e7Nr9DZSoRZw3pffkJP7Z0UjaA==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@nuxtjs/opencollective": "0.3.2", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "3.3.0", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/jwt": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.2.0.tgz", + "integrity": "sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "9.0.5", + "jsonwebtoken": "9.0.2" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/@nestjs/mapped-types": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.6.tgz", + "integrity": "sha512-84ze+CPfp1OWdpRi1/lOu59hOhTz38eVzJvRKrg9ykRFwDz+XleKfMsG0gUqNZYFa6v53XYzeD+xItt8uDW7NQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/mongoose": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/mongoose/-/mongoose-10.1.0.tgz", + "integrity": "sha512-1ExAnZUfh2QffEaGjqYGgVPy/sYBQCVLCLqVgkcClKx/BCd0QNgND8MB70lwyobp3nm/+nbGQqBpu9F3/hgOCw==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0", + "mongoose": "^6.0.2 || ^7.0.0 || ^8.0.0", + "rxjs": "^7.0.0" + } + }, + "node_modules/@nestjs/passport": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-10.0.3.tgz", + "integrity": "sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "passport": "^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0" + } + }, + "node_modules/@nestjs/platform-express": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.22.tgz", + "integrity": "sha512-ySSq7Py/DFozzZdNDH67m/vHoeVdphDniWBnl6q5QVoXldDdrZIHLXLRMPayTDh5A95nt7jjJzmD4qpTbNQ6tA==", + "license": "MIT", + "peer": true, + "dependencies": { + "body-parser": "1.20.4", + "cors": "2.8.5", + "express": "4.22.1", + "multer": "2.0.2", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0" + } + }, + "node_modules/@nestjs/platform-socket.io": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.22.tgz", + "integrity": "sha512-xxGw3R0Ihr51/Omq23z3//bKmCXyVKaikxbH0/pkwqMsQrxkUv9NabNUZ22b4Jnlwwi02X+zlwo8GRa9u8oV9g==", + "license": "MIT", + "dependencies": { + "socket.io": "4.8.1", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/platform-socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@nestjs/platform-socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@nestjs/platform-socket.io/node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/@nestjs/schematics": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.2.3.tgz", + "integrity": "sha512-4e8gxaCk7DhBxVUly2PjYL4xC2ifDFexCqq1/u4TtivLGXotVk0wHdYuPYe1tHTHuR1lsOkRbfOCpkdTnigLVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "comment-json": "4.2.5", + "jsonc-parser": "3.3.1", + "pluralize": "8.0.0" + }, + "peerDependencies": { + "typescript": ">=4.8.2" + } + }, + "node_modules/@nestjs/schematics/node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nestjs/swagger": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-8.1.1.tgz", + "integrity": "sha512-5Mda7H1DKnhKtlsb0C7PYshcvILv8UFyUotHzxmWh0G65Z21R3LZH/J8wmpnlzL4bmXIfr42YwbEwRxgzpJ5sQ==", + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "^0.15.0", + "@nestjs/mapped-types": "2.0.6", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "3.3.0", + "swagger-ui-dist": "5.18.2" + }, + "peerDependencies": { + "@fastify/static": "^6.0.0 || ^7.0.0", + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/testing": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.22.tgz", + "integrity": "sha512-HO9aPus3bAedAC+jKVAA8jTdaj4fs5M9fing4giHrcYV2txe9CvC1l1WAjwQ9RDhEHdugjY4y+FZA/U/YqPZrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + } + } + }, + "node_modules/@nestjs/websockets": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.22.tgz", + "integrity": "sha512-OLd4i0Faq7vgdtB5vVUrJ54hWEtcXy9poJ6n7kbbh/5ms+KffUl+wwGsbe7uSXLrkoyI8xXU6fZPkFArI+XiRg==", + "license": "MIT", + "peer": true, + "dependencies": { + "iterare": "1.2.1", + "object-hash": "3.0.0", + "tslib": "2.8.1" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/platform-socket.io": "^10.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/platform-socket.io": { + "optional": true + } + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nuxtjs/opencollective": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", + "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "consola": "^2.15.0", + "node-fetch": "^2.6.1" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/inflate/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@tokenizer/inflate/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", + "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/nodemailer": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.0.tgz", + "integrity": "sha512-fyf8jWULsCo0d0BuoQ75i6IeoHs47qcqxWc7yUdUcV0pOZGjUTTOvwdG1PRXUDqN/8A64yQdQdnA2pZgcdi+cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/oauth": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.6.tgz", + "integrity": "sha512-H9TRCVKBNOhZZmyHLqFt9drPM9l+ShWiqqJijU1B8P3DX3ub84NjxDuy+Hjrz+fEca5Kwip3qPMKNyiLgNJtIA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-google-oauth20": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.17.tgz", + "integrity": "sha512-MHNOd2l7gOTCn3iS+wInPQMiukliAUvMpODO3VlXxOiwNEMSyzV7UNvAdqxSN872o8OXx1SqPDVT6tLW74AtqQ==", + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-oauth2": "*" + } + }, + "node_modules/@types/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-6//z+4orIOy/g3zx17HyQ71GSRK4bs7Sb+zFasRoc2xzlv7ZCJ+vkDBYFci8U6HY+or6Zy7ajf4mz4rK7nsWJQ==", + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/oauth": "*", + "@types/passport": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.15.tgz", + "integrity": "sha512-1nfKCq9wuAZFTkA2ey/3OXXx7GzFjLdkTiFVNwlJ9WqdI706CZRIhEqjuwanjMIja+84jDLa9rcyZDPDiVkASQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001786", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001786.tgz", + "integrity": "sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT", + "peer": true + }, + "node_modules/class-validator": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.4.tgz", + "integrity": "sha512-AwNusCCam51q703dW82x95tOqQp6oC9HNUl724KxJJOfnKscI8dOloXFgyez7LbTTKWuRBA37FScqVbJEoq8Yw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.22" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/comment-json": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz", + "integrity": "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "core-util-is": "^1.0.3", + "esprima": "^4.0.1", + "has-own-prop": "^2.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/consola": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", + "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", + "license": "MIT" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.331", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz", + "integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.6.tgz", + "integrity": "sha512-U2SN0w3OpjFRVlrc17E6TMDmH58Xl9rai1MblNjAdwWp07Kk+llmzX0hjDpQdrDGzwmvOtgM5yI+meYX6iZ2xA==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", + "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.12" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-type": { + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.1.tgz", + "integrity": "sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.6", + "strtok3": "^10.2.0", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.0.2.tgz", + "integrity": "sha512-Uochze2R8peoN1XqlSi/rGUkDQpRogtLFocP9+PGu68zk1BDAKXfdeCdyVZpgTk8V8WFVQXdEz426VKjXLO1Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cosmiconfig": "^8.2.0", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=12.13.0", + "yarn": ">=1.0.0" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gaxios/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/gaxios/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/gaxios/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/gcp-metadata": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", + "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gcp-metadata/node_modules/gaxios": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", + "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/google-auth-library": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library/node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library/node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/google-auth-library/node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-own-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz", + "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/inquirer": { + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", + "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^6.0.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.2", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.12.41", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.41.tgz", + "integrity": "sha512-lsmMmGXBxXIK/VMLEj0kL6MtUs1kBGj1nTCzi6zgQoG1DEwqwt2DQyHxcLykceIxAnfE3hya7NuIh6PpC6S3fA==", + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mongodb": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.23.0.tgz", + "integrity": "sha512-Bul4Ha6J8IqzFrb0B1xpVzkC3S0sk43dmLSnhFOn8eJlZiLwL5WO6cRymmjaADdCMjUcCpj2ce8hZI6O4ZFSug==", + "license": "MIT", + "peer": true, + "dependencies": { + "bson": "^6.10.4", + "kareem": "2.6.3", + "mongodb": "~6.20.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/mquery/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mquery/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemailer": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz", + "integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/oauth": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", + "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "license": "MIT", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "license": "MIT", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", + "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.18.3" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/superagent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.2.tgz", + "integrity": "sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tapable": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/terser": { + "version": "5.46.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", + "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", + "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-jest": { + "version": "29.4.9", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.9.tgz", + "integrity": "sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.9", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.4", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <7" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-loader": { + "version": "9.5.7", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.7.tgz", + "integrity": "sha512-/ZNrKgA3K3PtpMYOC71EeMWIloGw3IYEa5/t1cyz2r5/PyUwTXGzYJvcD3kfUvmhlfpz1rhV8B2O6IVTQ0avsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", + "license": "MIT" + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/webpack": { + "version": "5.105.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", + "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f6bff5f --- /dev/null +++ b/package.json @@ -0,0 +1,67 @@ +{ + "name": "oudelaa-backend", + "version": "1.0.0", + "private": true, + "description": "Production-ready social media backend with NestJS and MongoDB", + "license": "UNLICENSED", + "scripts": { + "build": "nest build", + "start": "nest start", + "start:dev": "nest start --watch", + "start:prod": "node dist/main", + "lint": "eslint \"src/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:e2e": "jest --config ./test/jest-e2e.json" + }, + "dependencies": { + "@nestjs/common": "^10.4.0", + "@nestjs/config": "^3.2.3", + "@nestjs/core": "^10.4.0", + "@nestjs/jwt": "^10.2.0", + "@nestjs/mongoose": "^10.1.0", + "@nestjs/passport": "^10.0.3", + "@nestjs/platform-express": "^10.4.0", + "@nestjs/platform-socket.io": "^10.4.0", + "@nestjs/swagger": "^8.1.0", + "@nestjs/websockets": "^10.4.0", + "@types/passport-google-oauth20": "^2.0.17", + "bcrypt": "^5.1.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "google-auth-library": "^10.6.2", + "joi": "^17.13.3", + "mongoose": "^8.6.0", + "nodemailer": "^8.0.5", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", + "passport-jwt": "^4.0.1", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "socket.io": "^4.8.0" + }, + "devDependencies": { + "@nestjs/cli": "^10.4.5", + "@nestjs/schematics": "^10.2.3", + "@nestjs/testing": "^10.4.0", + "@types/bcrypt": "^5.0.2", + "@types/express": "^4.17.21", + "@types/jest": "^29.5.12", + "@types/node": "^20.16.5", + "@types/nodemailer": "^8.0.0", + "@types/passport-jwt": "^4.0.1", + "@types/supertest": "^6.0.2", + "eslint": "^9.11.1", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "jest": "^29.7.0", + "prettier": "^3.3.3", + "source-map-support": "^0.5.21", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-loader": "^9.5.1", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.6.2" + } +} diff --git a/postman/Oudelaa-Auth-Users-Posts.postman_collection.json b/postman/Oudelaa-Auth-Users-Posts.postman_collection.json new file mode 100644 index 0000000..4dbd597 --- /dev/null +++ b/postman/Oudelaa-Auth-Users-Posts.postman_collection.json @@ -0,0 +1,2745 @@ +{ + "info": { + "name": "Oudelaa Auth Users Posts", + "description": "Postman collection for auth, users, posts modules.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Auth", + "item": [ + { + "name": "Register Basic", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/register-basic", + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{registerEmail}}\",\n \"password\": \"StrongPass123!\",\n \"confirmPassword\": \"StrongPass123!\"\n}" + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "const ts = Date.now();", + "if (!pm.environment.get('registerEmail')) { pm.environment.set('registerEmail', `test_${ts}@example.com`); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.expect(json.message).to.exist;", + "pm.expect(json.email).to.exist;", + "if (json.debugCode) { pm.environment.set('emailVerificationCode', json.debugCode); }" + ] + } + } + ] + }, + { + "name": "Register", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/register", + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Test User\",\n \"stageName\": \"Artist One\",\n \"username\": \"{{registerUsername}}\",\n \"email\": \"{{registerEmail}}\",\n \"password\": \"StrongPass123!\",\n \"confirmPassword\": \"StrongPass123!\",\n \"musicRoles\": [\"singer\", \"composer\"],\n \"musicGenres\": [\"Tarab\", \"Pop\"],\n \"favoriteInstruments\": [\"Oud\", \"Piano\"],\n \"favoriteMaqamat\": [\"Bayati\", \"Rast\"],\n \"location\": \"Riyadh, Saudi Arabia\",\n \"latitude\": 24.7136,\n \"longitude\": 46.6753,\n \"isPrivate\": false\n}" + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "const ts = Date.now();", + "if (!pm.environment.get('registerEmail')) { pm.environment.set('registerEmail', `test_${ts}@example.com`); }", + "if (!pm.environment.get('registerUsername')) { pm.environment.set('registerUsername', `test_user_${ts}`); }" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.expect(json.message).to.exist;", + "pm.expect(json.email).to.exist;", + "if (json.debugCode) { pm.environment.set('emailVerificationCode', json.debugCode); }" + ] + } + } + ] + }, + { + "name": "Login", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/login", + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{registerEmail}}\",\n \"password\": \"StrongPass123!\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.user).to.exist;", + "const uid = json.user._id || json.user.id;", + "pm.environment.set('accessToken', json.accessToken);", + "pm.environment.set('refreshToken', json.refreshToken);", + "pm.environment.set('userId', uid);", + "pm.environment.set('currentUserId', uid);" + ] + } + } + ] + }, + { + "name": "Target User Login", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/login", + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{targetLoginEmail}}\",\n \"password\": \"{{targetLoginPassword}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.user).to.exist;", + "const uid = json.user._id || json.user.id;", + "pm.environment.set('targetAccessToken', json.accessToken);", + "pm.environment.set('targetRefreshToken', json.refreshToken);", + "pm.environment.set('targetUserId', uid);" + ] + } + } + ] + }, + { + "name": "Admin Login", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/login", + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{adminEmail}}\",\n \"password\": \"{{adminPassword}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.environment.set('adminAccessToken', json.accessToken);", + "pm.environment.set('adminRefreshToken', json.refreshToken);", + "pm.environment.set('adminUserId', (json.user && (json.user._id || json.user.id)) || pm.environment.get('adminUserId'));" + ] + } + } + ] + }, + { + "name": "Refresh", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/refresh", + "body": { + "mode": "raw", + "raw": "{\n \"refreshToken\": \"{{refreshToken}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.user).to.exist;", + "pm.environment.set('accessToken', json.accessToken);", + "pm.environment.set('refreshToken', json.refreshToken);", + "pm.environment.set('userId', json.user._id || json.user.id);" + ] + } + } + ] + }, + { + "name": "Logout", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/logout", + "body": { + "mode": "raw", + "raw": "{\n \"refreshToken\": \"{{refreshToken}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.message).to.eql('Logged out successfully');" + ] + } + } + ] + }, + { + "name": "SuperAdmin", + "item": [ + { + "name": "SuperAdmin Login", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/superadmin/login", + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"admin@oudelaa.com\",\n \"password\": \"SuperAdminStrongPass123!\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.accessToken).to.exist;", + "pm.expect(json.refreshToken).to.exist;", + "pm.environment.set('superAdminAccessToken', json.accessToken);", + "pm.environment.set('superAdminRefreshToken', json.refreshToken);" + ] + } + } + ] + }, + { + "name": "SuperAdmin Refresh", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/superadmin/refresh", + "body": { + "mode": "raw", + "raw": "{\n \"refreshToken\": \"{{superAdminRefreshToken}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.accessToken).to.exist;", + "pm.expect(json.refreshToken).to.exist;", + "pm.environment.set('superAdminAccessToken', json.accessToken);", + "pm.environment.set('superAdminRefreshToken', json.refreshToken);" + ] + } + } + ] + }, + { + "name": "SuperAdmin Logout", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/superadmin/logout", + "body": { + "mode": "raw", + "raw": "{\n \"refreshToken\": \"{{superAdminRefreshToken}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.message).to.eql('Superadmin logged out successfully');" + ] + } + } + ] + } + ] + }, + { + "name": "Google OAuth Start (Browser)", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/auth/google", + "description": "Open this request in browser flow to start Google OAuth redirect." + } + }, + { + "name": "Google OAuth Callback (Browser Redirect)", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/auth/google/callback", + "description": "Callback endpoint is called by Google redirect after browser login. Do not call directly from Postman." + } + }, + { + "name": "Google Token Login (Mobile - Recommended)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/google/token", + "description": "Mobile-style Google login (Instagram-like): send Google idToken from app SDK.", + "body": { + "mode": "raw", + "raw": "{\n \"idToken\": \"{{googleIdToken}}\"\n}" + } + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "const idToken = (pm.environment.get('googleIdToken') || '').trim();", + "if (!idToken) {", + " throw new Error('googleIdToken is empty. Paste Google idToken from Flutter/Google SDK into environment variable googleIdToken.');", + "}" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.accessToken).to.exist;", + "pm.expect(json.refreshToken).to.exist;", + "pm.expect(json.user).to.exist;", + "const uid = json.user._id || json.user.id;", + "pm.environment.set('accessToken', json.accessToken);", + "pm.environment.set('refreshToken', json.refreshToken);", + "pm.environment.set('userId', uid);", + "pm.environment.set('currentUserId', uid);" + ] + } + } + ] + }, + { + "name": "List My Sessions", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/auth/sessions" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an('array');", + "if (json.items && json.items.length > 0) { pm.environment.set('sessionJti', json.items[0].jti); }" + ] + } + } + ] + }, + { + "name": "Revoke My Session", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/auth/sessions/{{sessionJti}}/revoke" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.success).to.eql(true);" + ] + } + } + ] + }, + { + "name": "Forgot Password", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/forgot-password", + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{registerEmail}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.message).to.exist;", + "if (json.debugCode) { pm.environment.set('resetCode', json.debugCode); }" + ] + } + } + ] + }, + { + "name": "Verify Reset Code", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/verify-reset-code", + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{registerEmail}}\",\n \"code\": \"{{resetCode}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.resetToken).to.exist;", + "pm.environment.set('resetToken', json.resetToken);" + ] + } + } + ] + }, + { + "name": "Reset Password", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/reset-password", + "body": { + "mode": "raw", + "raw": "{\n \"resetToken\": \"{{resetToken}}\",\n \"newPassword\": \"{{newPassword}}\",\n \"confirmPassword\": \"{{newPassword}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.message).to.eql('Password reset successfully');" + ] + } + } + ] + }, + { + "name": "Send Email Verification", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/send-email-verification", + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{registerEmail}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.message).to.exist;", + "if (json.debugCode) { pm.environment.set('emailVerificationCode', json.debugCode); }" + ] + } + } + ] + }, + { + "name": "Verify Email", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/auth/verify-email", + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{registerEmail}}\",\n \"code\": \"{{emailVerificationCode}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.message).to.exist;", + "if (json.accessToken) { pm.environment.set('accessToken', json.accessToken); }", + "if (json.refreshToken) { pm.environment.set('refreshToken', json.refreshToken); }", + "if (json.user) { const uid = json.user._id || json.user.id; pm.environment.set('userId', uid); pm.environment.set('currentUserId', uid); }" + ] + } + } + ] + } + ] + }, + { + "name": "Users", + "item": [ + { + "name": "SuperAdmin List Admins", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": "{{baseUrl}}/users/admin/admins?page=1&limit=20" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an('array');", + "if (json.items && json.items.length > 0) { pm.environment.set('adminUserId', json.items[0]._id || json.items[0].id); }" + ] + } + } + ] + }, + { + "name": "SuperAdmin Update Admin", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": "{{baseUrl}}/users/admin/admins/{{adminUserId}}", + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Store Admin Updated\",\n \"stageName\": \"Music Store Owner\",\n \"bio\": \"Admin managing marketplace tools\",\n \"location\": \"Riyadh, Saudi Arabia\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property('role');", + "pm.expect(json.role).to.eql('admin');" + ] + } + } + ] + }, + { + "name": "SuperAdmin Delete Admin", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": "{{baseUrl}}/users/admin/admins/{{adminUserId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.message).to.eql('Admin deleted successfully');" + ] + } + } + ] + }, + { + "name": "SuperAdmin Create Admin", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": "{{baseUrl}}/users/admin/create-admin", + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Store Admin\",\n \"username\": \"{{adminUsername}}\",\n \"email\": \"{{adminEmail}}\",\n \"password\": \"{{adminPassword}}\",\n \"confirmPassword\": \"{{adminPassword}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.environment.set('adminUserId', json._id || json.id);" + ] + } + } + ] + }, + { + "name": "SuperAdmin Set User Role", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": "{{baseUrl}}/users/admin/{{adminUserId}}/role", + "body": { + "mode": "raw", + "raw": "{\n \"role\": \"admin\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.role).to.eql('admin');" + ] + } + } + ] + }, + { + "name": "Search Users", + "request": { + "method": "GET", + "url": "{{baseUrl}}/users?page=1&limit=10", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "const currentUserId = pm.environment.get('currentUserId');", + "if (json.items && json.items.length > 0) {", + " const target = json.items.find(u => (u._id || u.id) !== currentUserId) || json.items[0];", + " const targetId = target._id || target.id;", + " pm.environment.set('targetUserId', targetId);", + "}" + ] + } + } + ] + }, + { + "name": "Profile Setup", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/users/me/profile-setup", + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "stageName", + "value": "Artist One", + "type": "text" + }, + { + "key": "avatarFile", + "type": "file", + "src": [] + }, + { + "key": "bio", + "value": "Short bio", + "type": "text" + }, + { + "key": "location", + "value": "Riyadh, Saudi Arabia", + "type": "text" + }, + { + "key": "latitude", + "value": "24.7136", + "type": "text" + }, + { + "key": "longitude", + "value": "46.6753", + "type": "text" + } + ] + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.location).to.eql('Riyadh, Saudi Arabia');", + "pm.expect(json.latitude).to.eql(24.7136);", + "pm.expect(json.longitude).to.eql(46.6753);", + "if (json.avatar) { pm.expect(json.avatar).to.be.a('string'); }" + ] + } + } + ] + }, + { + "name": "Music Setup", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/users/me/music-setup", + "body": { + "mode": "raw", + "raw": "{\n \"musicRoles\": [\"instrumentalist\", \"composer\"],\n \"musicGenres\": [\"Tarab\", \"Pop\"],\n \"experienceLevel\": \"intermediate\",\n \"favoriteInstruments\": [\"Oud\", \"Piano\"],\n \"favoriteMaqamat\": [\"Bayati\", \"Rast\"]\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.musicRoles).to.include('instrumentalist');", + "pm.expect(json.musicRoles).to.include('composer');", + "pm.expect(json.experienceLevel).to.eql('intermediate');", + "pm.expect(json).to.not.have.property('isInstrumentalist');", + "pm.expect(json).to.not.have.property('isSinger');", + "pm.expect(json).to.not.have.property('isComposer');", + "pm.expect(json).to.not.have.property('isLyricist');" + ] + } + } + ] + }, + { + "name": "Update Me", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/users/me", + "body": { + "mode": "raw", + "raw": "{\n \"bio\": \"Updated from Postman\",\n \"avatar\": \"https://cdn.example.com/avatar.jpg\",\n \"stageName\": \"Artist One Updated\",\n \"musicGenres\": [\"Tarab\", \"Khaleeji\"],\n \"favoriteInstruments\": [\"Oud\"],\n \"favoriteMaqamat\": [\"Hijaz\"],\n \"location\": \"Jeddah, Saudi Arabia\"\n}" + } + } + }, + { + "name": "Get User By Id", + "request": { + "method": "GET", + "url": "{{baseUrl}}/users/{{userId}}", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ] + } + }, + { + "name": "Admin Get Users", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": "{{baseUrl}}/users/admin?page=1&limit=10" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "if (json.items && json.items.length > 0) { pm.environment.set('adminUserId', json.items[0]._id || json.items[0].id); }" + ] + } + } + ] + }, + { + "name": "Admin Get User By Id", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": "{{baseUrl}}/users/admin/{{adminUserId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json._id || json.id).to.exist;" + ] + } + } + ] + }, + { + "name": "Admin Update User", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": "{{baseUrl}}/users/admin/{{adminUserId}}", + "body": { + "mode": "raw", + "raw": "{\n \"stageName\": \"Updated by SuperAdmin\",\n \"bio\": \"Profile updated by admin\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.stageName).to.eql('Updated by SuperAdmin');" + ] + } + } + ] + }, + { + "name": "Admin Disable User", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": "{{baseUrl}}/users/admin/{{adminUserId}}/disable", + "body": { + "mode": "raw", + "raw": "{\n \"reason\": \"Violation of community guidelines\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.isDisabled).to.eql(true);" + ] + } + } + ] + }, + { + "name": "Admin Enable User", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": "{{baseUrl}}/users/admin/{{adminUserId}}/enable" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.isDisabled).to.eql(false);" + ] + } + } + ] + }, + { + "name": "Admin Delete User", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": "{{baseUrl}}/users/admin/{{adminUserId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.message).to.eql('User deleted successfully');" + ] + } + } + ] + } + ] + }, + { + "name": "Posts", + "item": [ + { + "name": "Create Post", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/posts", + "body": { + "mode": "raw", + "raw": "{\n \"content\": \"First text post\",\n \"visibility\": \"public\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.environment.set('postId', json._id || json.id);" + ] + } + } + ] + }, + { + "name": "Get User Posts", + "request": { + "method": "GET", + "url": "{{baseUrl}}/posts/user/{{userId}}?page=1&limit=10", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ] + } + }, + { + "name": "Get Post By Id", + "request": { + "method": "GET", + "url": "{{baseUrl}}/posts/{{postId}}", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ] + } + }, + { + "name": "Update Post", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/posts/{{postId}}", + "body": { + "mode": "raw", + "raw": "{\n \"content\": \"Updated post content\"\n}" + } + } + }, + { + "name": "Delete Post", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/posts/{{postId}}" + } + } + ] + }, + { + "name": "Comments", + "item": [ + { + "name": "Create Comment", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/comments", + "body": { + "mode": "raw", + "raw": "{\n \"postId\": \"{{postId}}\",\n \"content\": \"Awesome post!\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.environment.set('commentId', json._id || json.id);" + ] + } + } + ] + }, + { + "name": "Get Post Comments", + "request": { + "method": "GET", + "url": "{{baseUrl}}/comments/post/{{postId}}?page=1&limit=10", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" + ] + } + } + ] + }, + { + "name": "Get Comment Replies", + "request": { + "method": "GET", + "url": "{{baseUrl}}/comments/{{commentId}}/replies?page=1&limit=10", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" + ] + } + } + ] + }, + { + "name": "Delete Comment", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/comments/{{commentId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" + ] + } + } + ] + }, + { + "name": "Admin Delete Comment", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{superAdminAccessToken}}" + } + ], + "url": "{{baseUrl}}/comments/admin/{{commentId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.success).to.eql(true);" + ] + } + } + ] + } + ] + }, + { + "name": "Likes", + "item": [ + { + "name": "Like Post", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/likes", + "body": { + "mode": "raw", + "raw": "{\n \"targetId\": \"{{postId}}\",\n \"targetType\": \"post\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.expect(json.liked).to.eql(true);" + ] + } + } + ] + }, + { + "name": "Unlike Post", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/likes/post/{{postId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.liked).to.eql(false);" + ] + } + } + ] + }, + { + "name": "Get Post Like Status", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/likes/status/post/{{postId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property('liked');" + ] + } + } + ] + }, + { + "name": "Like Comment", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/likes", + "body": { + "mode": "raw", + "raw": "{\n \"targetId\": \"{{commentId}}\",\n \"targetType\": \"comment\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.expect(json.liked).to.eql(true);" + ] + } + } + ] + }, + { + "name": "Unlike Comment", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/likes/comment/{{commentId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.liked).to.eql(false);" + ] + } + } + ] + }, + { + "name": "Get Comment Like Status", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/likes/status/comment/{{commentId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property('liked');" + ] + } + } + ] + } + ] + }, + { + "name": "Saves", + "item": [ + { + "name": "Save Post", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/saves", + "body": { + "mode": "raw", + "raw": "{\n \"postId\": \"{{postId}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.expect(json.saved).to.eql(true);" + ] + } + } + ] + }, + { + "name": "Unsave Post", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/saves/{{postId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.saved).to.eql(false);" + ] + } + } + ] + }, + { + "name": "Get Save Status", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/saves/status/{{postId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property('saved');" + ] + } + } + ] + }, + { + "name": "Get My Saved Posts", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/saves/me?page=1&limit=10" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" + ] + } + } + ] + } + ] + }, + { + "name": "Follows", + "item": [ + { + "name": "Toggle Follow User", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/follows/toggle", + "body": { + "mode": "raw", + "raw": "{\n \"targetUserId\": \"{{targetUserId}}\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property('following');", + "pm.environment.set('lastFollowingState', json.following ? 'following' : 'not_following');" + ] + } + } + ] + }, + { + "name": "Get Followers", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/follows/followers/{{userId}}?page=1&limit=10" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" + ] + } + } + ] + }, + { + "name": "Get Following", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/follows/following/{{userId}}?page=1&limit=10" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" + ] + } + } + ] + }, + { + "name": "Get Follow Status", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/follows/status/{{targetUserId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property('following');" + ] + } + } + ] + }, + { + "name": "Get Follow Suggestions", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/follows/suggestions?page=1&limit=10" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an('array');" + ] + } + } + ] + } + ] + }, + { + "name": "Notifications", + "item": [ + { + "name": "Get My Notifications", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/notifications?page=1&limit=20" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an('array');", + "if (json.items && json.items.length > 0) { pm.environment.set('notificationId', json.items[0]._id || json.items[0].id); }" + ] + } + } + ] + }, + { + "name": "Get Target User Notifications", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{targetAccessToken}}" + } + ], + "url": "{{baseUrl}}/notifications?page=1&limit=20&read=false" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an('array');", + "pm.environment.set('notificationUnreadCount', json.unreadCount || 0);", + "const followNotification = (json.items || []).find(item => item.type === 'follow');", + "if (followNotification) { pm.environment.set('notificationId', followNotification._id || followNotification.id); }" + ] + } + } + ] + }, + { + "name": "Get Unread Notifications Count", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/notifications/unread-count" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property('unreadCount');" + ] + } + } + ] + }, + { + "name": "Get Target User Unread Notifications Count", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{targetAccessToken}}" + } + ], + "url": "{{baseUrl}}/notifications/unread-count" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property('unreadCount');", + "pm.environment.set('notificationUnreadCount', json.unreadCount || 0);" + ] + } + } + ] + }, + { + "name": "Mark Notification Read", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/notifications/{{notificationId}}/read" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property('unreadCount');" + ] + } + } + ] + }, + { + "name": "Mark Target Notification Read", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{targetAccessToken}}" + } + ], + "url": "{{baseUrl}}/notifications/{{notificationId}}/read" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property('unreadCount');", + "pm.environment.set('notificationUnreadCount', json.unreadCount || 0);" + ] + } + } + ] + }, + { + "name": "Mark All Notifications Read", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/notifications/read-all" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.unreadCount).to.eql(0);" + ] + } + } + ] + }, + { + "name": "Mark All Target Notifications Read", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{targetAccessToken}}" + } + ], + "url": "{{baseUrl}}/notifications/read-all" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.unreadCount).to.eql(0);", + "pm.environment.set('notificationUnreadCount', 0);" + ] + } + } + ] + } + ] + }, + { + "name": "Chat", + "item": [ + { + "name": "Create Conversation", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/chat/conversations", + "body": { + "mode": "raw", + "raw": "{\n \"participantIds\": [\"{{targetUserId}}\"],\n \"isGroup\": false\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.environment.set('conversationId', json._id || json.id);" + ] + } + } + ] + }, + { + "name": "Get My Conversations", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/chat/conversations?limit=20&cursor={{chatConversationsCursor}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "if (json.items && json.items.length > 0) { pm.environment.set('conversationId', json.items[0]._id || json.items[0].id); }", + "pm.environment.set('chatConversationsCursor', json.nextCursor || '');" + ] + } + } + ] + }, + { + "name": "Send Message", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/chat/messages", + "body": { + "mode": "raw", + "raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"messageType\": \"text\",\n \"content\": \"Hello from chat\"\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.environment.set('messageId', json._id || json.id);" + ] + } + } + ] + }, + { + "name": "Get Conversation Messages", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/chat/conversations/{{conversationId}}/messages?limit=20&cursor={{chatMessagesCursor}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.environment.set('chatMessagesCursor', json.nextCursor || '');" + ] + } + } + ] + }, + { + "name": "Mark Message Seen", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/chat/messages/{{messageId}}/seen" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" + ] + } + } + ] + }, + { + "name": "Unsend Message", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/chat/messages/{{messageId}}/unsend" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" + ] + } + } + ] + }, + { + "name": "Block User In Chat", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/chat/blocks/{{targetUserId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.expect(json.blocked).to.eql(true);" + ] + } + } + ] + }, + { + "name": "Unblock User In Chat", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/chat/blocks/{{targetUserId}}/unblock" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" + ] + } + } + ] + }, + { + "name": "Get Chat Block Status", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/chat/blocks/status/{{targetUserId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json).to.have.property('iBlocked');", + "pm.expect(json).to.have.property('blockedMe');" + ] + } + } + ] + }, + { + "name": "Get My Chat Blocks", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/chat/blocks" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an('array');" + ] + } + } + ] + } + ] + }, + { + "name": "Feed", + "item": [ + { + "name": "Get My Feed", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/feed/me?limit=20&cursor={{feedCursor}}&radiusKm=30" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an('array');", + "pm.environment.set('feedCursor', json.nextCursor || '');" + ] + } + } + ] + }, + { + "name": "Get My Feed Preferred Type", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/feed/me?limit=20&cursor={{feedCursor}}&preferredPostType=video&followingOnly=false&radiusKm=50" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.environment.set('feedCursor', json.nextCursor || '');" + ] + } + } + ] + }, + { + "name": "Get Trending Feed", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}" + } + ], + "url": "{{baseUrl}}/feed/trending?limit=20&cursor={{feedCursor}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an('array');", + "pm.environment.set('feedCursor', json.nextCursor || '');" + ] + } + } + ] + } + ] + }, + { + "name": "Marketplace", + "item": [ + { + "name": "Admin Create Instrument", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminAccessToken}}" + } + ], + "url": "{{baseUrl}}/marketplace/admin/instruments", + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"Professional Oud\",\n \"description\": \"Handmade oud for studio and stage\",\n \"price\": 4200,\n \"currency\": \"SAR\",\n \"quantity\": 3,\n \"imageUrls\": [\n \"https://cdn.example.com/instruments/oud-1.jpg\"\n ],\n \"isActive\": true\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 201', function () { pm.response.to.have.status(201); });", + "const json = pm.response.json();", + "pm.environment.set('instrumentId', json._id || json.id);" + ] + } + } + ] + }, + { + "name": "Admin Update Instrument", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminAccessToken}}" + } + ], + "url": "{{baseUrl}}/marketplace/admin/instruments/{{instrumentId}}", + "body": { + "mode": "raw", + "raw": "{\n \"price\": 3999,\n \"quantity\": 5,\n \"isActive\": true\n}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" + ] + } + } + ] + }, + { + "name": "Admin Get My Instruments", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{adminAccessToken}}" + } + ], + "url": "{{baseUrl}}/marketplace/admin/instruments/me?page=1&limit=20" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an('array');", + "if (json.items && json.items.length > 0) { pm.environment.set('instrumentId', json.items[0]._id || json.items[0].id); }" + ] + } + } + ] + }, + { + "name": "Public List Instruments", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/marketplace/instruments?page=1&limit=20&isActive=true" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.items).to.be.an('array');", + "if (json.items && json.items.length > 0) { pm.environment.set('instrumentId', json.items[0]._id || json.items[0].id); }" + ] + } + } + ] + }, + { + "name": "Public Get Instrument By Id", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/marketplace/instruments/{{instrumentId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });" + ] + } + } + ] + }, + { + "name": "Admin Delete Instrument", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{adminAccessToken}}" + } + ], + "url": "{{baseUrl}}/marketplace/admin/instruments/{{instrumentId}}" + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status is 200', function () { pm.response.to.have.status(200); });", + "const json = pm.response.json();", + "pm.expect(json.success).to.eql(true);" + ] + } + } + ] + } + ] + } + ], + "variable": [ + { + "key": "baseUrl", + "value": "http://192.168.1.9:4001/api/v1", + "type": "string" + }, + { + "key": "accessToken", + "value": "", + "type": "string" + }, + { + "key": "refreshToken", + "value": "", + "type": "string" + }, + { + "key": "superAdminAccessToken", + "value": "", + "type": "string" + }, + { + "key": "superAdminRefreshToken", + "value": "", + "type": "string" + }, + { + "key": "targetAccessToken", + "value": "", + "type": "string" + }, + { + "key": "targetRefreshToken", + "value": "", + "type": "string" + }, + { + "key": "userId", + "value": "", + "type": "string" + }, + { + "key": "currentUserId", + "value": "", + "type": "string" + }, + { + "key": "targetUserId", + "value": "", + "type": "string" + }, + { + "key": "lastFollowingState", + "value": "", + "type": "string" + }, + { + "key": "notificationUnreadCount", + "value": "", + "type": "string" + }, + { + "key": "notificationId", + "value": "", + "type": "string" + }, + { + "key": "postId", + "value": "", + "type": "string" + }, + { + "key": "commentId", + "value": "", + "type": "string" + }, + { + "key": "conversationId", + "value": "", + "type": "string" + }, + { + "key": "messageId", + "value": "", + "type": "string" + }, + { + "key": "sessionJti", + "value": "", + "type": "string" + }, + { + "key": "registerEmail", + "value": "", + "type": "string" + }, + { + "key": "registerUsername", + "value": "", + "type": "string" + }, + { + "key": "adminUserId", + "value": "", + "type": "string" + }, + { + "key": "targetLoginEmail", + "value": "", + "type": "string" + }, + { + "key": "targetLoginPassword", + "value": "", + "type": "string" + }, + { + "key": "resetCode", + "value": "", + "type": "string" + }, + { + "key": "resetToken", + "value": "", + "type": "string" + }, + { + "key": "emailVerificationCode", + "value": "", + "type": "string" + }, + { + "key": "googleIdToken", + "value": "", + "type": "string" + }, + { + "key": "newPassword", + "value": "NewStrongPass123!", + "type": "string" + }, + { + "key": "feedCursor", + "value": "", + "type": "string" + }, + { + "key": "chatConversationsCursor", + "value": "", + "type": "string" + }, + { + "key": "chatMessagesCursor", + "value": "", + "type": "string" + }, + { + "key": "adminAccessToken", + "value": "", + "type": "string" + }, + { + "key": "adminRefreshToken", + "value": "", + "type": "string" + }, + { + "key": "adminEmail", + "value": "store_admin@example.com", + "type": "string" + }, + { + "key": "adminUsername", + "value": "store_admin_01", + "type": "string" + }, + { + "key": "adminPassword", + "value": "AdminStrongPass123!", + "type": "string" + }, + { + "key": "instrumentId", + "value": "", + "type": "string" + } + ] +} diff --git a/postman/Oudelaa-Local.postman_environment.json b/postman/Oudelaa-Local.postman_environment.json new file mode 100644 index 0000000..18619ee --- /dev/null +++ b/postman/Oudelaa-Local.postman_environment.json @@ -0,0 +1,39 @@ +{ + "id": "c5f5b9cc-9e95-4e19-9a76-cc8f6b95a001", + "name": "Oudelaa Local", + "values": [ + { + "key": "baseUrl", + "value": "http://localhost:4000", + "type": "default", + "enabled": true + }, + { + "key": "accessToken", + "value": "", + "type": "secret", + "enabled": true + }, + { + "key": "refreshToken", + "value": "", + "type": "secret", + "enabled": true + }, + { + "key": "postId", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "userId", + "value": "", + "type": "default", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2026-04-06T00:00:00.000Z", + "_postman_exported_using": "Codex" +} diff --git a/postman/superadmin-dashboard-config.json b/postman/superadmin-dashboard-config.json new file mode 100644 index 0000000..08d52e4 --- /dev/null +++ b/postman/superadmin-dashboard-config.json @@ -0,0 +1,209 @@ +{ + "dashboard": { + "name": "Oudelaa SuperAdmin Dashboard", + "version": "1.2.0", + "baseUrl": "{{baseUrl}}", + "auth": { + "type": "Bearer", + "login": { + "method": "POST", + "url": "/auth/superadmin/login", + "body": { + "email": "admin@oudelaa.com", + "password": "SuperAdminStrongPass123!" + }, + "responseTokens": { + "accessToken": "superAdminAccessToken", + "refreshToken": "superAdminRefreshToken" + } + }, + "refresh": { + "method": "POST", + "url": "/auth/superadmin/refresh", + "body": { + "refreshToken": "{{superAdminRefreshToken}}" + } + }, + "logout": { + "method": "POST", + "url": "/auth/superadmin/logout", + "body": { + "refreshToken": "{{superAdminRefreshToken}}" + } + } + }, + "modules": [ + { + "key": "usersModeration", + "title": "Users Moderation", + "endpoints": [ + { + "name": "Admin Get Users", + "method": "GET", + "url": "/users/admin?page=1&limit=10", + "headers": { + "Authorization": "Bearer {{superAdminAccessToken}}" + } + }, + { + "name": "Admin Get User By Id", + "method": "GET", + "url": "/users/admin/:userId", + "headers": { + "Authorization": "Bearer {{superAdminAccessToken}}" + } + }, + { + "name": "Admin Update User", + "method": "PATCH", + "url": "/users/admin/:userId", + "headers": { + "Authorization": "Bearer {{superAdminAccessToken}}", + "Content-Type": "application/json" + }, + "body": { + "stageName": "Updated by SuperAdmin", + "bio": "Profile updated by admin" + } + }, + { + "name": "Disable User", + "method": "PATCH", + "url": "/users/admin/:userId/disable", + "headers": { + "Authorization": "Bearer {{superAdminAccessToken}}", + "Content-Type": "application/json" + }, + "body": { + "reason": "Violation of community guidelines" + } + }, + { + "name": "Enable User", + "method": "PATCH", + "url": "/users/admin/:userId/enable", + "headers": { + "Authorization": "Bearer {{superAdminAccessToken}}" + } + }, + { + "name": "Delete User", + "method": "DELETE", + "url": "/users/admin/:userId", + "headers": { + "Authorization": "Bearer {{superAdminAccessToken}}" + } + } + ] + }, + { + "key": "commentsModeration", + "title": "Comments Moderation", + "endpoints": [ + { + "name": "Admin Delete Comment", + "method": "DELETE", + "url": "/comments/admin/:commentId", + "headers": { + "Authorization": "Bearer {{superAdminAccessToken}}" + } + } + ] + }, + { + "key": "securitySessions", + "title": "Security & Sessions", + "endpoints": [ + { + "name": "My Sessions", + "method": "GET", + "url": "/auth/sessions", + "headers": { + "Authorization": "Bearer {{accessToken}}" + } + }, + { + "name": "Revoke Session", + "method": "POST", + "url": "/auth/sessions/:jti/revoke", + "headers": { + "Authorization": "Bearer {{accessToken}}" + } + } + ] + }, + { + "key": "feedAlgorithms", + "title": "Feed Algorithms", + "endpoints": [ + { + "name": "My Feed", + "method": "GET", + "url": "/feed/me?page=1&limit=20&radiusKm=30", + "headers": { + "Authorization": "Bearer {{accessToken}}" + } + }, + { + "name": "My Feed Preferred", + "method": "GET", + "url": "/feed/me?page=1&limit=20&preferredPostType=video&followingOnly=false&radiusKm=50", + "headers": { + "Authorization": "Bearer {{accessToken}}" + } + }, + { + "name": "Trending Feed", + "method": "GET", + "url": "/feed/trending?page=1&limit=20", + "headers": { + "Authorization": "Bearer {{accessToken}}" + } + } + ] + } + ], + "state": { + "tokens": [ + "superAdminAccessToken", + "superAdminRefreshToken", + "sessionJti", + "targetUserId", + "conversationId", + "messageId", + "accessToken" + ], + "selectedUser": "userId", + "selectedComment": "commentId" + }, + "ui": { + "pages": [ + "SuperAdmin Login", + "Users List", + "User Profile", + "Update User", + "Disable User Modal", + "Delete User Confirmation", + "Comments Moderation", + "Feed Ranking", + "Security Sessions" + ], + "tables": [ + { + "id": "users", + "columns": [ + "_id", + "name", + "stageName", + "username", + "email", + "role", + "isDisabled", + "disabledReason", + "createdAt" + ] + } + ] + } + } +} diff --git a/src/app.controller.ts b/src/app.controller.ts new file mode 100644 index 0000000..646fe3f --- /dev/null +++ b/src/app.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Get } from '@nestjs/common'; +import { AppService } from './app.service'; + +@Controller('health') +export class AppController { + constructor(private readonly appService: AppService) {} + + @Get() + getHealth(): { status: string; service: string } { + return this.appService.getHealth(); + } +} diff --git a/src/app.module.ts b/src/app.module.ts new file mode 100644 index 0000000..b5adde8 --- /dev/null +++ b/src/app.module.ts @@ -0,0 +1,58 @@ +import { Module } from '@nestjs/common'; +import { APP_GUARD } from '@nestjs/core'; +import { ConfigModule } from '@nestjs/config'; +import { AppController } from './app.controller'; +import { AppService } from './app.service'; +import configuration from './config/configuration'; +import { validationSchema } from './config/validation.schema'; +import { DatabaseModule } from './database/database.module'; +import { AuthModule } from './modules/auth/auth.module'; +import { AuditModule } from './modules/audit/audit.module'; +import { ChatModule } from './modules/chat/chat.module'; +import { CommentsModule } from './modules/comments/comments.module'; +import { FeedModule } from './modules/feed/feed.module'; +import { FollowsModule } from './modules/follows/follows.module'; +import { LikesModule } from './modules/likes/likes.module'; +import { MediaModule } from './modules/media/media.module'; +import { MarketplaceModule } from './modules/marketplace/marketplace.module'; +import { NotificationsModule } from './modules/notifications/notifications.module'; +import { OutboxModule } from './modules/outbox/outbox.module'; +import { PostsModule } from './modules/posts/posts.module'; +import { SavesModule } from './modules/saves/saves.module'; +import { UsersModule } from './modules/users/users.module'; +import { ThrottleGuard } from './common/guards/throttle.guard'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + cache: true, + load: [configuration], + validationSchema, + }), + DatabaseModule, + AuditModule, + UsersModule, + AuthModule, + PostsModule, + CommentsModule, + LikesModule, + FollowsModule, + FeedModule, + NotificationsModule, + OutboxModule, + ChatModule, + MediaModule, + MarketplaceModule, + SavesModule, + ], + controllers: [AppController], + providers: [ + AppService, + { + provide: APP_GUARD, + useClass: ThrottleGuard, + }, + ], +}) +export class AppModule {} diff --git a/src/app.service.ts b/src/app.service.ts new file mode 100644 index 0000000..08e7ba8 --- /dev/null +++ b/src/app.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AppService { + getHealth(): { status: string; service: string } { + return { status: 'ok', service: 'oudelaa-backend' }; + } +} diff --git a/src/common/decorators/current-user.decorator.ts b/src/common/decorators/current-user.decorator.ts new file mode 100644 index 0000000..db797bb --- /dev/null +++ b/src/common/decorators/current-user.decorator.ts @@ -0,0 +1,6 @@ +import { ExecutionContext, createParamDecorator } from '@nestjs/common'; + +export const CurrentUser = createParamDecorator((_: unknown, context: ExecutionContext) => { + const request = context.switchToHttp().getRequest(); + return request.user; +}); diff --git a/src/common/decorators/public.decorator.ts b/src/common/decorators/public.decorator.ts new file mode 100644 index 0000000..f56344d --- /dev/null +++ b/src/common/decorators/public.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; + +export const IS_PUBLIC_KEY = 'isPublic'; +export const Public = (): ReturnType => SetMetadata(IS_PUBLIC_KEY, true); diff --git a/src/common/decorators/roles.decorator.ts b/src/common/decorators/roles.decorator.ts new file mode 100644 index 0000000..725666e --- /dev/null +++ b/src/common/decorators/roles.decorator.ts @@ -0,0 +1,5 @@ +import { SetMetadata } from '@nestjs/common'; + +export const ROLES_KEY = 'roles'; +export const Roles = (...roles: string[]): ReturnType => + SetMetadata(ROLES_KEY, roles); diff --git a/src/common/decorators/throttle.decorator.ts b/src/common/decorators/throttle.decorator.ts new file mode 100644 index 0000000..428d2c7 --- /dev/null +++ b/src/common/decorators/throttle.decorator.ts @@ -0,0 +1,11 @@ +import { SetMetadata } from '@nestjs/common'; + +export const THROTTLE_META_KEY = 'throttle_meta_key'; + +export type ThrottleMeta = { + limit: number; + windowMs: number; +}; + +export const Throttle = (limit: number, windowMs: number) => + SetMetadata(THROTTLE_META_KEY, { limit, windowMs } satisfies ThrottleMeta); diff --git a/src/common/dto/object-id-param.dto.ts b/src/common/dto/object-id-param.dto.ts new file mode 100644 index 0000000..4e8c934 --- /dev/null +++ b/src/common/dto/object-id-param.dto.ts @@ -0,0 +1,6 @@ +import { IsMongoId } from 'class-validator'; + +export class ObjectIdParamDto { + @IsMongoId() + id!: string; +} diff --git a/src/common/dto/pagination-query.dto.ts b/src/common/dto/pagination-query.dto.ts new file mode 100644 index 0000000..38f4cf2 --- /dev/null +++ b/src/common/dto/pagination-query.dto.ts @@ -0,0 +1,22 @@ +import { Type } from 'class-transformer'; +import { IsInt, IsOptional, IsString, Max, Min } from 'class-validator'; +import { APP_CONSTANTS } from '../../config/constants'; + +export class PaginationQueryDto { + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page?: number = APP_CONSTANTS.DEFAULT_PAGE; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(APP_CONSTANTS.MAX_LIMIT) + limit?: number = APP_CONSTANTS.DEFAULT_LIMIT; + + @IsOptional() + @IsString() + cursor?: string; +} diff --git a/src/common/enums/experience-level.enum.ts b/src/common/enums/experience-level.enum.ts new file mode 100644 index 0000000..d3017b1 --- /dev/null +++ b/src/common/enums/experience-level.enum.ts @@ -0,0 +1,6 @@ +export enum ExperienceLevel { + BEGINNER = 'beginner', + INTERMEDIATE = 'intermediate', + ADVANCED = 'advanced', + PROFESSIONAL = 'professional', +} diff --git a/src/common/enums/music-role.enum.ts b/src/common/enums/music-role.enum.ts new file mode 100644 index 0000000..6d08928 --- /dev/null +++ b/src/common/enums/music-role.enum.ts @@ -0,0 +1,8 @@ +export enum MusicRole { + INSTRUMENTALIST = 'instrumentalist', + SINGER = 'singer', + COMPOSER = 'composer', + LYRICIST = 'lyricist', + PRODUCER = 'producer', + ARRANGER = 'arranger', +} diff --git a/src/common/enums/notification-type.enum.ts b/src/common/enums/notification-type.enum.ts new file mode 100644 index 0000000..2aba8d8 --- /dev/null +++ b/src/common/enums/notification-type.enum.ts @@ -0,0 +1,6 @@ +export enum NotificationType { + LIKE = 'like', + COMMENT = 'comment', + FOLLOW = 'follow', + MESSAGE = 'message', +} diff --git a/src/common/enums/post-type.enum.ts b/src/common/enums/post-type.enum.ts new file mode 100644 index 0000000..994cd65 --- /dev/null +++ b/src/common/enums/post-type.enum.ts @@ -0,0 +1,5 @@ +export enum PostType { + TEXT = 'text', + VIDEO = 'video', + AUDIO = 'audio', +} diff --git a/src/common/enums/post-visibility.enum.ts b/src/common/enums/post-visibility.enum.ts new file mode 100644 index 0000000..36332c3 --- /dev/null +++ b/src/common/enums/post-visibility.enum.ts @@ -0,0 +1,5 @@ +export enum PostVisibility { + PUBLIC = 'public', + FOLLOWERS = 'followers', + PRIVATE = 'private', +} diff --git a/src/common/enums/token-type.enum.ts b/src/common/enums/token-type.enum.ts new file mode 100644 index 0000000..702bee3 --- /dev/null +++ b/src/common/enums/token-type.enum.ts @@ -0,0 +1,4 @@ +export enum TokenType { + ACCESS = 'access', + REFRESH = 'refresh', +} diff --git a/src/common/enums/user-role.enum.ts b/src/common/enums/user-role.enum.ts new file mode 100644 index 0000000..a1de200 --- /dev/null +++ b/src/common/enums/user-role.enum.ts @@ -0,0 +1,5 @@ +export enum UserRole { + USER = 'user', + ADMIN = 'admin', + SUPERADMIN = 'superadmin', +} diff --git a/src/common/guards/jwt-auth.guard.ts b/src/common/guards/jwt-auth.guard.ts new file mode 100644 index 0000000..2155290 --- /dev/null +++ b/src/common/guards/jwt-auth.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') {} diff --git a/src/common/guards/jwt-refresh.guard.ts b/src/common/guards/jwt-refresh.guard.ts new file mode 100644 index 0000000..ed74420 --- /dev/null +++ b/src/common/guards/jwt-refresh.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class JwtRefreshGuard extends AuthGuard('jwt-refresh') {} diff --git a/src/common/guards/roles.guard.ts b/src/common/guards/roles.guard.ts new file mode 100644 index 0000000..52daac1 --- /dev/null +++ b/src/common/guards/roles.guard.ts @@ -0,0 +1,29 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { ROLES_KEY } from '../decorators/roles.decorator'; + +@Injectable() +export class RolesGuard implements CanActivate { + constructor(private readonly reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const requiredRoles = this.reflector.getAllAndOverride(ROLES_KEY, [ + context.getHandler(), + context.getClass(), + ]); + + if (!requiredRoles || requiredRoles.length === 0) { + return true; + } + + const request = context.switchToHttp().getRequest(); + const payload = request.user ?? {}; + const userRoles: string[] = Array.isArray(payload.roles) + ? payload.roles + : payload.role + ? [payload.role] + : []; + + return requiredRoles.some((role) => userRoles.includes(role)); + } +} diff --git a/src/common/guards/super-admin-jwt-auth.guard.ts b/src/common/guards/super-admin-jwt-auth.guard.ts new file mode 100644 index 0000000..51ecd55 --- /dev/null +++ b/src/common/guards/super-admin-jwt-auth.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class SuperAdminJwtAuthGuard extends AuthGuard('superadmin-jwt') {} diff --git a/src/common/guards/throttle.guard.ts b/src/common/guards/throttle.guard.ts new file mode 100644 index 0000000..b3edf79 --- /dev/null +++ b/src/common/guards/throttle.guard.ts @@ -0,0 +1,50 @@ +import { + CanActivate, + ExecutionContext, + HttpException, + HttpStatus, + Injectable, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { THROTTLE_META_KEY, ThrottleMeta } from '../decorators/throttle.decorator'; + +type Bucket = { + count: number; + resetAt: number; +}; + +@Injectable() +export class ThrottleGuard implements CanActivate { + private readonly buckets = new Map(); + + constructor(private readonly reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const meta = this.reflector.getAllAndOverride(THROTTLE_META_KEY, [ + context.getHandler(), + context.getClass(), + ]); + if (!meta) { + return true; + } + + const req = context.switchToHttp().getRequest(); + const ip = req.ip ?? 'unknown'; + const route = req.originalUrl ?? 'unknown-route'; + const key = `${ip}:${route}`; + const now = Date.now(); + const existing = this.buckets.get(key); + + if (!existing || now > existing.resetAt) { + this.buckets.set(key, { count: 1, resetAt: now + meta.windowMs }); + return true; + } + + if (existing.count >= meta.limit) { + throw new HttpException('Too many requests, please try again later', HttpStatus.TOO_MANY_REQUESTS); + } + + existing.count += 1; + return true; + } +} diff --git a/src/common/interceptors/response-envelope.interceptor.ts b/src/common/interceptors/response-envelope.interceptor.ts new file mode 100644 index 0000000..115123e --- /dev/null +++ b/src/common/interceptors/response-envelope.interceptor.ts @@ -0,0 +1,25 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, +} from '@nestjs/common'; +import { map, Observable } from 'rxjs'; + +@Injectable() +export class ResponseEnvelopeInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const http = context.switchToHttp(); + const response = http.getResponse<{ statusCode?: number }>(); + + return next.handle().pipe( + map((data) => ({ + data, + meta: { + statusCode: response.statusCode ?? 200, + timestamp: new Date().toISOString(), + }, + })), + ); + } +} diff --git a/src/common/interfaces/jwt-payload.interface.ts b/src/common/interfaces/jwt-payload.interface.ts new file mode 100644 index 0000000..ed2ff9f --- /dev/null +++ b/src/common/interfaces/jwt-payload.interface.ts @@ -0,0 +1,7 @@ +export interface JwtPayload { + sub: string; + username: string; + role?: string; + tokenType: 'access' | 'refresh' | 'superadmin_access' | 'superadmin_refresh'; + email?: string; +} diff --git a/src/common/utils/cursor.util.spec.ts b/src/common/utils/cursor.util.spec.ts new file mode 100644 index 0000000..3a641a4 --- /dev/null +++ b/src/common/utils/cursor.util.spec.ts @@ -0,0 +1,13 @@ +import { decodeOffsetCursor, encodeOffsetCursor } from './cursor.util'; + +describe('cursor util', () => { + it('encodes and decodes cursor offsets', () => { + const cursor = encodeOffsetCursor(40); + expect(decodeOffsetCursor(cursor)).toBe(40); + }); + + it('returns null on invalid cursor', () => { + expect(decodeOffsetCursor('%%%invalid%%%')).toBeNull(); + expect(decodeOffsetCursor(encodeOffsetCursor(-1))).toBeNull(); + }); +}); diff --git a/src/common/utils/cursor.util.ts b/src/common/utils/cursor.util.ts new file mode 100644 index 0000000..13d8183 --- /dev/null +++ b/src/common/utils/cursor.util.ts @@ -0,0 +1,19 @@ +export const encodeOffsetCursor = (offset: number): string => + Buffer.from(String(offset), 'utf8').toString('base64url'); + +export const decodeOffsetCursor = (cursor?: string): number | null => { + if (!cursor) { + return null; + } + + try { + const raw = Buffer.from(cursor, 'base64url').toString('utf8'); + const parsed = Number(raw); + if (!Number.isInteger(parsed) || parsed < 0) { + return null; + } + return parsed; + } catch { + return null; + } +}; diff --git a/src/common/utils/hash.util.ts b/src/common/utils/hash.util.ts new file mode 100644 index 0000000..a4ff91a --- /dev/null +++ b/src/common/utils/hash.util.ts @@ -0,0 +1,7 @@ +import * as bcrypt from 'bcrypt'; + +export const hashValue = async (value: string, saltRounds: number): Promise => + bcrypt.hash(value, saltRounds); + +export const compareHash = async (value: string, hashedValue: string): Promise => + bcrypt.compare(value, hashedValue); diff --git a/src/config/configuration.ts b/src/config/configuration.ts new file mode 100644 index 0000000..051a469 --- /dev/null +++ b/src/config/configuration.ts @@ -0,0 +1,69 @@ +export default () => ({ + nodeEnv: process.env.NODE_ENV ?? 'development', + port: Number(process.env.PORT ?? 4000), + host: process.env.HOST ?? '0.0.0.0', + publicBaseUrl: + process.env.PUBLIC_BASE_URL ?? + `http://localhost:${Number(process.env.PORT ?? 4000)}`, + responseEnvelopeEnabled: + (process.env.RESPONSE_ENVELOPE_ENABLED ?? 'false').toLowerCase() === 'true', + globalPrefix: process.env.GLOBAL_PREFIX ?? 'api/v1', + cors: { + origins: (process.env.CORS_ORIGINS ?? '') + .split(',') + .map((origin) => origin.trim()) + .filter((origin) => origin.length > 0), + }, + mongodb: { + uri: process.env.MONGODB_URI ?? 'mongodb://127.0.0.1:27017/oudelaa', + }, + jwt: { + accessSecret: process.env.JWT_ACCESS_SECRET ?? '', + accessExpiresIn: process.env.JWT_ACCESS_EXPIRES_IN ?? '15m', + refreshSecret: process.env.JWT_REFRESH_SECRET ?? '', + refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN ?? '30d', + }, + superAdmin: { + email: (process.env.SUPERADMIN_EMAIL ?? '').toLowerCase(), + password: process.env.SUPERADMIN_PASSWORD ?? '', + accessSecret: process.env.SUPERADMIN_ACCESS_SECRET ?? '', + accessExpiresIn: process.env.SUPERADMIN_ACCESS_EXPIRES_IN ?? '15m', + refreshSecret: process.env.SUPERADMIN_REFRESH_SECRET ?? '', + refreshExpiresIn: process.env.SUPERADMIN_REFRESH_EXPIRES_IN ?? '30d', + }, + google: { + clientId: process.env.GOOGLE_CLIENT_ID ?? '', + clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? '', + callbackUrl: + process.env.GOOGLE_CALLBACK_URL ?? 'http://localhost:4000/api/v1/auth/google/callback', + }, + email: { + enabled: (process.env.EMAIL_ENABLED ?? 'false').toLowerCase() === 'true', + smtpHost: process.env.EMAIL_SMTP_HOST ?? 'smtp.gmail.com', + smtpPort: Number(process.env.EMAIL_SMTP_PORT ?? 587), + smtpSecure: (process.env.EMAIL_SMTP_SECURE ?? 'false').toLowerCase() === 'true', + smtpUser: process.env.EMAIL_SMTP_USER ?? '', + smtpPass: process.env.EMAIL_SMTP_PASS ?? '', + fromName: process.env.EMAIL_FROM_NAME ?? 'Oudelaa', + fromEmail: process.env.EMAIL_FROM_EMAIL ?? process.env.EMAIL_SMTP_USER ?? '', + }, + security: { + bcryptSaltRounds: Number(process.env.BCRYPT_SALT_ROUNDS ?? 12), + }, + passwordReset: { + codeExpiresMinutes: Number(process.env.PASSWORD_RESET_CODE_EXPIRES_MINUTES ?? 10), + maxAttempts: Number(process.env.PASSWORD_RESET_MAX_ATTEMPTS ?? 5), + tokenSecret: process.env.PASSWORD_RESET_TOKEN_SECRET ?? process.env.JWT_ACCESS_SECRET ?? '', + tokenExpiresIn: process.env.PASSWORD_RESET_TOKEN_EXPIRES_IN ?? '15m', + }, + emailVerification: { + codeExpiresMinutes: Number(process.env.EMAIL_VERIFICATION_CODE_EXPIRES_MINUTES ?? 10), + maxAttempts: Number(process.env.EMAIL_VERIFICATION_MAX_ATTEMPTS ?? 5), + }, + swagger: { + title: process.env.SWAGGER_TITLE ?? 'Oudelaa API', + description: process.env.SWAGGER_DESCRIPTION ?? 'Social media backend API documentation', + version: process.env.SWAGGER_VERSION ?? '1.0.0', + path: process.env.SWAGGER_PATH ?? 'docs', + }, +}); diff --git a/src/config/constants.ts b/src/config/constants.ts new file mode 100644 index 0000000..bea43f8 --- /dev/null +++ b/src/config/constants.ts @@ -0,0 +1,5 @@ +export const APP_CONSTANTS = { + DEFAULT_PAGE: 1, + DEFAULT_LIMIT: 20, + MAX_LIMIT: 100, +}; diff --git a/src/config/swagger.config.ts b/src/config/swagger.config.ts new file mode 100644 index 0000000..9afc466 --- /dev/null +++ b/src/config/swagger.config.ts @@ -0,0 +1,17 @@ +import { INestApplication } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; + +export const setupSwagger = (app: INestApplication, configService: ConfigService): void => { + const config = new DocumentBuilder() + .setTitle(configService.get('swagger.title', 'Oudelaa API')) + .setDescription( + configService.get('swagger.description', 'Social media backend API documentation'), + ) + .setVersion(configService.get('swagger.version', '1.0.0')) + .addBearerAuth() + .build(); + + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup(configService.get('swagger.path', 'docs'), app, document); +}; diff --git a/src/config/validation.schema.ts b/src/config/validation.schema.ts new file mode 100644 index 0000000..3b52d0f --- /dev/null +++ b/src/config/validation.schema.ts @@ -0,0 +1,44 @@ +import * as Joi from 'joi'; + +export const validationSchema = Joi.object({ + NODE_ENV: Joi.string().valid('development', 'test', 'production').default('development'), + PORT: Joi.number().default(4000), + HOST: Joi.string().default('0.0.0.0'), + PUBLIC_BASE_URL: Joi.string().uri().optional(), + RESPONSE_ENVELOPE_ENABLED: Joi.boolean().truthy('true').falsy('false').default(false), + GLOBAL_PREFIX: Joi.string().default('api/v1'), + CORS_ORIGINS: Joi.string().allow('').optional(), + MONGODB_URI: Joi.string().required(), + JWT_ACCESS_SECRET: Joi.string().min(16).required(), + JWT_ACCESS_EXPIRES_IN: Joi.string().default('15m'), + JWT_REFRESH_SECRET: Joi.string().min(16).required(), + JWT_REFRESH_EXPIRES_IN: Joi.string().default('30d'), + SUPERADMIN_EMAIL: Joi.string().email().required(), + SUPERADMIN_PASSWORD: Joi.string().min(8).required(), + SUPERADMIN_ACCESS_SECRET: Joi.string().min(16).required(), + SUPERADMIN_ACCESS_EXPIRES_IN: Joi.string().default('15m'), + SUPERADMIN_REFRESH_SECRET: Joi.string().min(16).required(), + SUPERADMIN_REFRESH_EXPIRES_IN: Joi.string().default('30d'), + GOOGLE_CLIENT_ID: Joi.string().allow('').optional(), + GOOGLE_CLIENT_SECRET: Joi.string().allow('').optional(), + GOOGLE_CALLBACK_URL: Joi.string().uri().optional(), + EMAIL_ENABLED: Joi.boolean().truthy('true').falsy('false').default(false), + EMAIL_SMTP_HOST: Joi.string().allow('').optional(), + EMAIL_SMTP_PORT: Joi.number().default(587), + EMAIL_SMTP_SECURE: Joi.boolean().truthy('true').falsy('false').default(false), + EMAIL_SMTP_USER: Joi.string().allow('').optional(), + EMAIL_SMTP_PASS: Joi.string().allow('').optional(), + EMAIL_FROM_NAME: Joi.string().default('Oudelaa'), + EMAIL_FROM_EMAIL: Joi.string().allow('').optional(), + BCRYPT_SALT_ROUNDS: Joi.number().min(8).max(15).default(12), + PASSWORD_RESET_CODE_EXPIRES_MINUTES: Joi.number().min(1).max(60).default(10), + PASSWORD_RESET_MAX_ATTEMPTS: Joi.number().min(1).max(10).default(5), + PASSWORD_RESET_TOKEN_SECRET: Joi.string().allow('').optional(), + PASSWORD_RESET_TOKEN_EXPIRES_IN: Joi.string().default('15m'), + EMAIL_VERIFICATION_CODE_EXPIRES_MINUTES: Joi.number().min(1).max(60).default(10), + EMAIL_VERIFICATION_MAX_ATTEMPTS: Joi.number().min(1).max(10).default(5), + SWAGGER_TITLE: Joi.string().default('Oudelaa API'), + SWAGGER_DESCRIPTION: Joi.string().default('Social media backend API documentation'), + SWAGGER_VERSION: Joi.string().default('1.0.0'), + SWAGGER_PATH: Joi.string().default('docs'), +}); diff --git a/src/database/database.module.ts b/src/database/database.module.ts new file mode 100644 index 0000000..890af55 --- /dev/null +++ b/src/database/database.module.ts @@ -0,0 +1,16 @@ +import { Global, Module } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { MongooseModule } from '@nestjs/mongoose'; +import { createMongooseOptions } from './mongoose-options.factory'; + +@Global() +@Module({ + imports: [ + MongooseModule.forRootAsync({ + inject: [ConfigService], + useFactory: (configService: ConfigService) => createMongooseOptions(configService), + }), + ], + exports: [MongooseModule], +}) +export class DatabaseModule {} diff --git a/src/database/mongoose-options.factory.ts b/src/database/mongoose-options.factory.ts new file mode 100644 index 0000000..c4d9b09 --- /dev/null +++ b/src/database/mongoose-options.factory.ts @@ -0,0 +1,7 @@ +import { ConfigService } from '@nestjs/config'; +import { MongooseModuleOptions } from '@nestjs/mongoose'; + +export const createMongooseOptions = (configService: ConfigService): MongooseModuleOptions => ({ + uri: configService.get('mongodb.uri', { infer: true }), + autoIndex: true, +}); diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..a22c0e3 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,84 @@ +import { ValidationPipe } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; +import { ConfigService } from '@nestjs/config'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import * as express from 'express'; +import { randomUUID } from 'crypto'; +import { NextFunction, Request, Response } from 'express'; +import { existsSync, mkdirSync } from 'fs'; +import { join } from 'path'; +import { AppModule } from './app.module'; +import { ResponseEnvelopeInterceptor } from './common/interceptors/response-envelope.interceptor'; + +async function bootstrap(): Promise { + const app = await NestFactory.create(AppModule); + const configService = app.get(ConfigService); + const corsOrigins = configService.get('cors.origins', []); + const uploadsDir = join(process.cwd(), 'uploads'); + + if (!existsSync(uploadsDir)) { + mkdirSync(uploadsDir, { recursive: true }); + } + + app.enableCors({ + origin: corsOrigins.length ? corsOrigins : true, + credentials: true, + }); + + app.setGlobalPrefix(configService.get('globalPrefix', 'api/v1')); + + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + transformOptions: { enableImplicitConversion: true }, + }), + ); + + app.use((req: Request, res: Response, next: NextFunction) => { + const startedAt = Date.now(); + const requestId = (req.headers['x-request-id'] as string | undefined) ?? randomUUID(); + req.headers['x-request-id'] = requestId; + res.setHeader('x-request-id', requestId); + + res.on('finish', () => { + const log = { + level: 'info', + requestId, + method: req.method, + path: req.originalUrl, + statusCode: res.statusCode, + durationMs: Date.now() - startedAt, + }; + console.log(JSON.stringify(log)); + }); + + next(); + }); + + const responseEnvelopeEnabled = configService.get('responseEnvelopeEnabled', false); + if (responseEnvelopeEnabled) { + app.useGlobalInterceptors(new ResponseEnvelopeInterceptor()); + } + + app.use('/uploads', express.static(uploadsDir)); + + const swaggerConfig = new DocumentBuilder() + .setTitle(configService.get('swagger.title', 'Oudelaa API')) + .setDescription( + configService.get('swagger.description', 'Social media backend API documentation'), + ) + .setVersion(configService.get('swagger.version', '1.0.0')) + .addBearerAuth() + .build(); + + const document = SwaggerModule.createDocument(app, swaggerConfig); + SwaggerModule.setup(configService.get('swagger.path', 'docs'), app, document); + + const port = configService.get('port', 4000); + const host = configService.get('host', '0.0.0.0'); + await app.listen(port, host); +} + +void bootstrap(); diff --git a/src/modules/audit/audit.module.ts b/src/modules/audit/audit.module.ts new file mode 100644 index 0000000..83447c2 --- /dev/null +++ b/src/modules/audit/audit.module.ts @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { AuditRepository } from './audit.repository'; +import { AuditService } from './audit.service'; +import { AuditLog, AuditLogSchema } from './schemas/audit-log.schema'; + +@Module({ + imports: [ + MongooseModule.forFeature([ + { + name: AuditLog.name, + schema: AuditLogSchema, + }, + ]), + ], + providers: [AuditRepository, AuditService], + exports: [AuditService], +}) +export class AuditModule {} diff --git a/src/modules/audit/audit.repository.ts b/src/modules/audit/audit.repository.ts new file mode 100644 index 0000000..63fd6f2 --- /dev/null +++ b/src/modules/audit/audit.repository.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model, Types } from 'mongoose'; +import { AuditLog, AuditLogDocument } from './schemas/audit-log.schema'; + +@Injectable() +export class AuditRepository { + constructor(@InjectModel(AuditLog.name) private readonly auditModel: Model) {} + + async create(payload: { + actorType: 'user' | 'superadmin' | 'system'; + actorUserId?: string; + actorIdentifier?: string; + action: string; + targetType: string; + targetId?: string; + metadata?: Record; + }): Promise { + await this.auditModel.create({ + actorType: payload.actorType, + ...(payload.actorUserId ? { actorUserId: new Types.ObjectId(payload.actorUserId) } : {}), + actorIdentifier: payload.actorIdentifier, + action: payload.action, + targetType: payload.targetType, + targetId: payload.targetId, + metadata: payload.metadata ?? {}, + }); + } +} diff --git a/src/modules/audit/audit.service.ts b/src/modules/audit/audit.service.ts new file mode 100644 index 0000000..6818607 --- /dev/null +++ b/src/modules/audit/audit.service.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@nestjs/common'; +import { AuditRepository } from './audit.repository'; + +@Injectable() +export class AuditService { + constructor(private readonly auditRepository: AuditRepository) {} + + async logSuperAdminAction( + actorIdentifier: string, + action: string, + targetType: string, + targetId?: string, + metadata?: Record, + ): Promise { + await this.auditRepository.create({ + actorType: 'superadmin', + actorIdentifier, + action, + targetType, + targetId, + metadata, + }); + } +} diff --git a/src/modules/audit/schemas/audit-log.schema.ts b/src/modules/audit/schemas/audit-log.schema.ts new file mode 100644 index 0000000..b622db2 --- /dev/null +++ b/src/modules/audit/schemas/audit-log.schema.ts @@ -0,0 +1,31 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { HydratedDocument, Types } from 'mongoose'; + +export type AuditLogDocument = HydratedDocument; + +@Schema({ timestamps: true, versionKey: false }) +export class AuditLog { + @Prop({ required: true, index: true }) + actorType!: 'user' | 'superadmin' | 'system'; + + @Prop({ type: Types.ObjectId, required: false, index: true }) + actorUserId?: Types.ObjectId; + + @Prop({ type: String, required: false, index: true }) + actorIdentifier?: string; + + @Prop({ required: true, index: true }) + action!: string; + + @Prop({ required: true, index: true }) + targetType!: string; + + @Prop({ type: String, required: false, index: true }) + targetId?: string; + + @Prop({ type: Object, default: {} }) + metadata!: Record; +} + +export const AuditLogSchema = SchemaFactory.createForClass(AuditLog); +AuditLogSchema.index({ createdAt: -1, action: 1 }); diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts new file mode 100644 index 0000000..d08ec01 --- /dev/null +++ b/src/modules/auth/auth.controller.ts @@ -0,0 +1,154 @@ +import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Req, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { Request } from 'express'; +import { Throttle } from '../../common/decorators/throttle.decorator'; +import { CurrentUser } from '../../common/decorators/current-user.decorator'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { JwtPayload } from '../../common/interfaces/jwt-payload.interface'; +import { AuthService } from './auth.service'; +import { ForgotPasswordDto } from './dto/forgot-password.dto'; +import { GoogleTokenLoginDto } from './dto/google-token-login.dto'; +import { LoginDto } from './dto/login.dto'; +import { RegisterBasicDto } from './dto/register-basic.dto'; +import { ResetPasswordDto } from './dto/reset-password.dto'; +import { RefreshTokenDto } from './dto/refresh-token.dto'; +import { GoogleAuthGuard } from './guards/google-auth.guard'; +import { RegisterDto } from './dto/register.dto'; +import { SendEmailVerificationDto } from './dto/send-email-verification.dto'; +import { SuperAdminLoginDto } from './dto/super-admin-login.dto'; +import { VerifyEmailDto } from './dto/verify-email.dto'; +import { VerifyResetCodeDto } from './dto/verify-reset-code.dto'; + +@ApiTags('Auth') +@Controller('auth') +export class AuthController { + constructor(private readonly authService: AuthService) {} + + @Post('register') + @Throttle(10, 60_000) + async register(@Body() dto: RegisterDto) { + return this.authService.register(dto); + } + + @Post('register-basic') + @Throttle(10, 60_000) + async registerBasic(@Body() dto: RegisterBasicDto) { + return this.authService.registerBasic(dto); + } + + @HttpCode(HttpStatus.OK) + @Post('login') + @Throttle(20, 60_000) + async login(@Body() dto: LoginDto) { + return this.authService.login(dto); + } + + @HttpCode(HttpStatus.OK) + @Post('refresh') + @Throttle(30, 60_000) + async refresh(@Body() dto: RefreshTokenDto) { + return this.authService.refresh(dto); + } + + @HttpCode(HttpStatus.OK) + @Post('logout') + async logout(@Body() dto: RefreshTokenDto): Promise<{ message: string }> { + await this.authService.logout(dto); + return { message: 'Logged out successfully' }; + } + + @HttpCode(HttpStatus.OK) + @Post('forgot-password') + @Throttle(8, 60_000) + async forgotPassword(@Body() dto: ForgotPasswordDto) { + return this.authService.forgotPassword(dto); + } + + @HttpCode(HttpStatus.OK) + @Post('verify-reset-code') + @Throttle(20, 60_000) + async verifyResetCode(@Body() dto: VerifyResetCodeDto) { + return this.authService.verifyResetCode(dto); + } + + @HttpCode(HttpStatus.OK) + @Post('reset-password') + @Throttle(10, 60_000) + async resetPassword(@Body() dto: ResetPasswordDto) { + return this.authService.resetPassword(dto); + } + + @HttpCode(HttpStatus.OK) + @Post('send-email-verification') + @Throttle(8, 60_000) + async sendEmailVerification(@Body() dto: SendEmailVerificationDto) { + return this.authService.sendEmailVerification(dto); + } + + @HttpCode(HttpStatus.OK) + @Post('verify-email') + @Throttle(20, 60_000) + async verifyEmail(@Body() dto: VerifyEmailDto) { + return this.authService.verifyEmail(dto); + } + + @HttpCode(HttpStatus.OK) + @Post('superadmin/login') + @Throttle(10, 60_000) + async superAdminLogin(@Body() dto: SuperAdminLoginDto) { + return this.authService.superAdminLogin(dto); + } + + @HttpCode(HttpStatus.OK) + @Post('superadmin/refresh') + @Throttle(20, 60_000) + async superAdminRefresh(@Body() dto: RefreshTokenDto) { + return this.authService.superAdminRefresh(dto); + } + + @HttpCode(HttpStatus.OK) + @Post('superadmin/logout') + async superAdminLogout(@Body() dto: RefreshTokenDto): Promise<{ message: string }> { + await this.authService.superAdminLogout(dto); + return { message: 'Superadmin logged out successfully' }; + } + + @Get('google') + @UseGuards(GoogleAuthGuard) + async googleAuth(): Promise { + return; + } + + @Get('google/callback') + @UseGuards(GoogleAuthGuard) + async googleCallback( + @Req() + req: Request & { + user: { googleId: string; email: string; name: string; avatar?: string }; + }, + ) { + return this.authService.loginWithGoogle(req.user); + } + + @HttpCode(HttpStatus.OK) + @Post('google/token') + @Throttle(20, 60_000) + async googleTokenLogin(@Body() dto: GoogleTokenLoginDto) { + return this.authService.loginWithGoogleIdToken(dto); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('sessions') + async listMySessions(@CurrentUser() user: JwtPayload) { + return this.authService.listUserSessions(user.sub); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post('sessions/:jti/revoke') + async revokeSession(@CurrentUser() user: JwtPayload, @Param('jti') jti: string) { + await this.authService.revokeUserSession(user.sub, jti); + return { success: true }; + } +} diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts new file mode 100644 index 0000000..3c724a9 --- /dev/null +++ b/src/modules/auth/auth.module.ts @@ -0,0 +1,73 @@ +import { Module } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { JwtModule } from '@nestjs/jwt'; +import { MongooseModule } from '@nestjs/mongoose'; +import { EmailModule } from '../email/email.module'; +import { UsersModule } from '../users/users.module'; +import { AuthController } from './auth.controller'; +import { AuthRepository } from './auth.repository'; +import { AuthService } from './auth.service'; +import { GoogleAuthGuard } from './guards/google-auth.guard'; +import { + EmailVerificationCode, + EmailVerificationCodeSchema, +} from './schemas/email-verification-code.schema'; +import { + PasswordResetCode, + PasswordResetCodeSchema, +} from './schemas/password-reset-code.schema'; +import { RefreshToken, RefreshTokenSchema } from './schemas/refresh-token.schema'; +import { + SuperAdminRefreshToken, + SuperAdminRefreshTokenSchema, +} from './schemas/super-admin-refresh-token.schema'; +import { GoogleStrategy } from './strategies/google.strategy'; +import { JwtRefreshStrategy } from './strategies/jwt-refresh.strategy'; +import { JwtStrategy } from './strategies/jwt.strategy'; +import { SuperAdminJwtStrategy } from './strategies/super-admin-jwt.strategy'; + +@Module({ + imports: [ + JwtModule.registerAsync({ + inject: [ConfigService], + useFactory: (configService: ConfigService) => ({ + secret: configService.get('jwt.accessSecret', { infer: true }), + signOptions: { + expiresIn: configService.get('jwt.accessExpiresIn', { infer: true }), + }, + }), + }), + MongooseModule.forFeature([ + { + name: RefreshToken.name, + schema: RefreshTokenSchema, + }, + { + name: SuperAdminRefreshToken.name, + schema: SuperAdminRefreshTokenSchema, + }, + { + name: PasswordResetCode.name, + schema: PasswordResetCodeSchema, + }, + { + name: EmailVerificationCode.name, + schema: EmailVerificationCodeSchema, + }, + ]), + EmailModule, + UsersModule, + ], + controllers: [AuthController], + providers: [ + AuthService, + AuthRepository, + JwtStrategy, + JwtRefreshStrategy, + GoogleStrategy, + GoogleAuthGuard, + SuperAdminJwtStrategy, + ], + exports: [AuthService], +}) +export class AuthModule {} diff --git a/src/modules/auth/auth.repository.ts b/src/modules/auth/auth.repository.ts new file mode 100644 index 0000000..e6c3c98 --- /dev/null +++ b/src/modules/auth/auth.repository.ts @@ -0,0 +1,237 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model, Types } from 'mongoose'; +import { + EmailVerificationCode, + EmailVerificationCodeDocument, +} from './schemas/email-verification-code.schema'; +import { + PasswordResetCode, + PasswordResetCodeDocument, +} from './schemas/password-reset-code.schema'; +import { RefreshToken, RefreshTokenDocument } from './schemas/refresh-token.schema'; +import { + SuperAdminRefreshToken, + SuperAdminRefreshTokenDocument, +} from './schemas/super-admin-refresh-token.schema'; + +@Injectable() +export class AuthRepository { + constructor( + @InjectModel(RefreshToken.name) + private readonly refreshTokenModel: Model, + @InjectModel(SuperAdminRefreshToken.name) + private readonly superAdminRefreshTokenModel: Model, + @InjectModel(PasswordResetCode.name) + private readonly passwordResetCodeModel: Model, + @InjectModel(EmailVerificationCode.name) + private readonly emailVerificationCodeModel: Model, + ) {} + + async createRefreshToken(userId: string, jti: string, tokenHash: string, expiresAt: Date): Promise { + await this.refreshTokenModel.create({ + userId: new Types.ObjectId(userId), + jti, + tokenHash, + expiresAt, + }); + } + + async findActiveUserTokens(userId: string): Promise { + return this.refreshTokenModel + .find({ userId: new Types.ObjectId(userId), revoked: false }) + .select('+tokenHash') + .exec(); + } + + async revokeAllUserTokens(userId: string): Promise { + await this.refreshTokenModel + .updateMany({ userId: new Types.ObjectId(userId), revoked: false }, { revoked: true }) + .exec(); + } + + async revokeUserTokenByJti(userId: string, jti: string): Promise { + await this.refreshTokenModel + .updateOne({ userId: new Types.ObjectId(userId), jti, revoked: false }, { revoked: true }) + .exec(); + } + + async findActiveUserTokenByJti(userId: string, jti: string): Promise { + return this.refreshTokenModel + .findOne({ userId: new Types.ObjectId(userId), jti, revoked: false }) + .select('+tokenHash') + .exec(); + } + + async markCompromisedAndRevokeAll(userId: string): Promise { + await this.refreshTokenModel + .updateMany( + { userId: new Types.ObjectId(userId), revoked: false }, + { revoked: true, compromised: true }, + ) + .exec(); + } + + async listUserSessions(userId: string): Promise { + return this.refreshTokenModel + .find({ userId: new Types.ObjectId(userId), revoked: false, expiresAt: { $gt: new Date() } }) + .select('jti expiresAt createdAt') + .sort({ createdAt: -1 }) + .exec(); + } + + async removeExpiredAndRevoked(userId: string): Promise { + await this.refreshTokenModel + .deleteMany({ + userId: new Types.ObjectId(userId), + $or: [{ revoked: true }, { expiresAt: { $lt: new Date() } }], + }) + .exec(); + } + + async createSuperAdminRefreshToken( + adminEmail: string, + tokenHash: string, + expiresAt: Date, + ): Promise { + await this.superAdminRefreshTokenModel.create({ + adminEmail: adminEmail.toLowerCase(), + tokenHash, + expiresAt, + }); + } + + async findActiveSuperAdminTokens(adminEmail: string): Promise { + return this.superAdminRefreshTokenModel + .find({ adminEmail: adminEmail.toLowerCase(), revoked: false }) + .select('+tokenHash') + .exec(); + } + + async revokeAllSuperAdminTokens(adminEmail: string): Promise { + await this.superAdminRefreshTokenModel + .updateMany({ adminEmail: adminEmail.toLowerCase(), revoked: false }, { revoked: true }) + .exec(); + } + + async removeExpiredAndRevokedSuperAdmin(adminEmail: string): Promise { + await this.superAdminRefreshTokenModel + .deleteMany({ + adminEmail: adminEmail.toLowerCase(), + $or: [{ revoked: true }, { expiresAt: { $lt: new Date() } }], + }) + .exec(); + } + + async invalidateActivePasswordResetCodes(userId: string): Promise { + await this.passwordResetCodeModel + .updateMany( + { userId: new Types.ObjectId(userId), used: false, expiresAt: { $gt: new Date() } }, + { used: true }, + ) + .exec(); + } + + async createPasswordResetCode(userId: string, codeHash: string, expiresAt: Date): Promise { + await this.passwordResetCodeModel.create({ + userId: new Types.ObjectId(userId), + codeHash, + expiresAt, + attempts: 0, + verified: false, + used: false, + }); + } + + async findLatestActivePasswordResetCode(userId: string): Promise { + return this.passwordResetCodeModel + .findOne({ + userId: new Types.ObjectId(userId), + used: false, + expiresAt: { $gt: new Date() }, + }) + .select('+codeHash') + .sort({ createdAt: -1 }) + .exec(); + } + + async incrementPasswordResetAttempts(id: string): Promise { + await this.passwordResetCodeModel.findByIdAndUpdate(id, { $inc: { attempts: 1 } }).exec(); + } + + async markPasswordResetCodeVerified(id: string): Promise { + await this.passwordResetCodeModel.findByIdAndUpdate(id, { verified: true }).exec(); + } + + async markPasswordResetCodeUsed(id: string): Promise { + await this.passwordResetCodeModel.findByIdAndUpdate(id, { used: true }).exec(); + } + + async markPasswordResetCodeUsedByUser(userId: string): Promise { + await this.passwordResetCodeModel + .updateMany({ userId: new Types.ObjectId(userId), used: false }, { used: true }) + .exec(); + } + + async findValidVerifiedPasswordResetCode( + codeId: string, + userId: string, + ): Promise { + return this.passwordResetCodeModel + .findOne({ + _id: new Types.ObjectId(codeId), + userId: new Types.ObjectId(userId), + used: false, + verified: true, + expiresAt: { $gt: new Date() }, + }) + .exec(); + } + + async invalidateActiveEmailVerificationCodes(userId: string): Promise { + await this.emailVerificationCodeModel + .updateMany( + { userId: new Types.ObjectId(userId), used: false, expiresAt: { $gt: new Date() } }, + { used: true }, + ) + .exec(); + } + + async createEmailVerificationCode(userId: string, codeHash: string, expiresAt: Date): Promise { + await this.emailVerificationCodeModel.create({ + userId: new Types.ObjectId(userId), + codeHash, + expiresAt, + attempts: 0, + used: false, + }); + } + + async findLatestActiveEmailVerificationCode( + userId: string, + ): Promise { + return this.emailVerificationCodeModel + .findOne({ + userId: new Types.ObjectId(userId), + used: false, + expiresAt: { $gt: new Date() }, + }) + .select('+codeHash') + .sort({ createdAt: -1 }) + .exec(); + } + + async incrementEmailVerificationAttempts(id: string): Promise { + await this.emailVerificationCodeModel.findByIdAndUpdate(id, { $inc: { attempts: 1 } }).exec(); + } + + async markEmailVerificationCodeUsed(id: string): Promise { + await this.emailVerificationCodeModel.findByIdAndUpdate(id, { used: true }).exec(); + } + + async markAllEmailVerificationCodesUsedByUser(userId: string): Promise { + await this.emailVerificationCodeModel + .updateMany({ userId: new Types.ObjectId(userId), used: false }, { used: true }) + .exec(); + } +} diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts new file mode 100644 index 0000000..af3b8c1 --- /dev/null +++ b/src/modules/auth/auth.service.ts @@ -0,0 +1,666 @@ +import { + BadRequestException, + ForbiddenException, + Injectable, + Logger, + UnauthorizedException, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { JwtService } from '@nestjs/jwt'; +import { randomBytes, randomInt, randomUUID } from 'crypto'; +import { OAuth2Client } from 'google-auth-library'; +import { compareHash, hashValue } from '../../common/utils/hash.util'; +import { EmailService } from '../email/email.service'; +import { UsersService } from '../users/users.service'; +import { ForgotPasswordDto } from './dto/forgot-password.dto'; +import { GoogleTokenLoginDto } from './dto/google-token-login.dto'; +import { LoginDto } from './dto/login.dto'; +import { RegisterBasicDto } from './dto/register-basic.dto'; +import { ResetPasswordDto } from './dto/reset-password.dto'; +import { RefreshTokenDto } from './dto/refresh-token.dto'; +import { RegisterDto } from './dto/register.dto'; +import { SendEmailVerificationDto } from './dto/send-email-verification.dto'; +import { SuperAdminLoginDto } from './dto/super-admin-login.dto'; +import { VerifyEmailDto } from './dto/verify-email.dto'; +import { VerifyResetCodeDto } from './dto/verify-reset-code.dto'; +import { AuthRepository } from './auth.repository'; +import { AuthResult, TokenPair } from './types/token-pair.type'; + +@Injectable() +export class AuthService { + private readonly googleOAuthClient = new OAuth2Client(); + private readonly logger = new Logger(AuthService.name); + + constructor( + private readonly usersService: UsersService, + private readonly authRepository: AuthRepository, + private readonly jwtService: JwtService, + private readonly configService: ConfigService, + private readonly emailService: EmailService, + ) {} + + async register(dto: RegisterDto): Promise<{ message: string; email: string; debugCode?: string }> { + if (dto.password !== dto.confirmPassword) { + throw new BadRequestException('Password confirmation does not match'); + } + + const saltRounds = this.configService.get('security.bcryptSaltRounds', { infer: true }); + const passwordHash = await hashValue(dto.password, saltRounds); + const generatedUsername = dto.username ?? (await this.generateUniqueUsernameFromEmail(dto.email)); + const resolvedName = dto.name ?? dto.stageName ?? generatedUsername; + + const { confirmPassword: _, ...registerPayload } = dto; + const user = await this.usersService.create({ + ...registerPayload, + name: resolvedName, + username: generatedUsername, + password: passwordHash, + }); + const code = await this.issueEmailVerificationCode(user.id, user.email); + const response: { message: string; email: string; debugCode?: string } = { + message: 'Registration successful. Verify your email with the code sent.', + email: user.email, + }; + const nodeEnv = this.configService.get('nodeEnv', { infer: true }); + if (nodeEnv !== 'production') { + response.debugCode = code; + } + return response; + } + + async registerBasic(dto: RegisterBasicDto): Promise<{ message: string; email: string; debugCode?: string }> { + if (dto.password !== dto.confirmPassword) { + throw new BadRequestException('Password confirmation does not match'); + } + + const saltRounds = this.configService.get('security.bcryptSaltRounds', { infer: true }); + const passwordHash = await hashValue(dto.password, saltRounds); + const generatedUsername = await this.generateUniqueUsernameFromEmail(dto.email); + + const user = await this.usersService.create({ + name: generatedUsername, + username: generatedUsername, + email: dto.email, + password: passwordHash, + }); + + const code = await this.issueEmailVerificationCode(user.id, user.email); + const response: { message: string; email: string; debugCode?: string } = { + message: 'Registration successful. Verify your email with the code sent.', + email: user.email, + }; + const nodeEnv = this.configService.get('nodeEnv', { infer: true }); + if (nodeEnv !== 'production') { + response.debugCode = code; + } + return response; + } + + async login(dto: LoginDto): Promise { + const user = await this.usersService.findByEmailWithPassword(dto.email); + if (!user || !user.password) { + throw new UnauthorizedException('Invalid credentials'); + } + if (user.isDisabled) { + throw new ForbiddenException('Account is disabled'); + } + if (!user.isVerified) { + throw new ForbiddenException('Email not verified'); + } + + const isMatch = await compareHash(dto.password, user.password); + if (!isMatch) { + throw new UnauthorizedException('Invalid credentials'); + } + + const tokens = await this.generateAndStoreTokenPair(user.id, user.username, user.role ?? 'user'); + const safeUser = await this.usersService.findByIdOrFail(user.id); + return { ...tokens, user: safeUser.toObject() as unknown as Record }; + } + + async sendEmailVerification( + dto: SendEmailVerificationDto, + ): Promise<{ message: string; debugCode?: string }> { + const normalizedEmail = dto.email.toLowerCase(); + const user = await this.usersService.findByEmail(normalizedEmail); + const message = 'If this email exists, a verification code was sent'; + if (!user || user.isDisabled) { + return { message }; + } + if (user.isVerified) { + return { message: 'Email is already verified' }; + } + + const code = await this.issueEmailVerificationCode(user.id, user.email); + const response: { message: string; debugCode?: string } = { message }; + const nodeEnv = this.configService.get('nodeEnv', { infer: true }); + if (nodeEnv !== 'production') { + response.debugCode = code; + } + return response; + } + + async verifyEmail(dto: VerifyEmailDto): Promise { + const normalizedEmail = dto.email.toLowerCase(); + const user = await this.usersService.findByEmail(normalizedEmail); + if (!user || user.isDisabled) { + throw new UnauthorizedException('Invalid or expired verification code'); + } + + if (user.isVerified) { + const tokens = await this.generateAndStoreTokenPair(user.id, user.username, user.role ?? 'user'); + const safeUser = await this.usersService.findByIdOrFail(user.id); + return { + message: 'Email already verified', + ...tokens, + user: safeUser.toObject() as unknown as Record, + }; + } + + const codeRecord = await this.authRepository.findLatestActiveEmailVerificationCode(user.id); + if (!codeRecord) { + throw new UnauthorizedException('Invalid or expired verification code'); + } + + const maxAttempts = this.configService.get('emailVerification.maxAttempts', { infer: true }); + if (codeRecord.attempts >= maxAttempts) { + await this.authRepository.markEmailVerificationCodeUsed(codeRecord.id); + throw new UnauthorizedException('Verification code attempts exceeded'); + } + + const isMatch = await compareHash(dto.code, codeRecord.codeHash); + if (!isMatch) { + await this.authRepository.incrementEmailVerificationAttempts(codeRecord.id); + if (codeRecord.attempts + 1 >= maxAttempts) { + await this.authRepository.markEmailVerificationCodeUsed(codeRecord.id); + } + throw new UnauthorizedException('Invalid or expired verification code'); + } + + await this.usersService.markEmailVerified(user.id); + await this.authRepository.markEmailVerificationCodeUsed(codeRecord.id); + await this.authRepository.markAllEmailVerificationCodesUsedByUser(user.id); + + const safeUser = await this.usersService.findByIdOrFail(user.id); + const tokens = await this.generateAndStoreTokenPair(safeUser.id, safeUser.username, safeUser.role ?? 'user'); + + return { + message: 'Email verified successfully', + ...tokens, + user: safeUser.toObject() as unknown as Record, + }; + } + + async refresh(dto: RefreshTokenDto): Promise { + const decoded = this.jwtService.verify<{ sub: string; username: string; tokenType: string; jti?: string }>( + dto.refreshToken, + { + secret: this.configService.get('jwt.refreshSecret', { infer: true }), + }, + ); + + if (decoded.tokenType !== 'refresh' || !decoded.jti) { + throw new UnauthorizedException('Invalid refresh token'); + } + + const tokenRecord = await this.authRepository.findActiveUserTokenByJti(decoded.sub, decoded.jti); + if (!tokenRecord) { + await this.authRepository.markCompromisedAndRevokeAll(decoded.sub); + throw new UnauthorizedException('Refresh token reuse detected'); + } + + const isMatch = await compareHash(dto.refreshToken, tokenRecord.tokenHash); + if (!isMatch) { + await this.authRepository.markCompromisedAndRevokeAll(decoded.sub); + throw new UnauthorizedException('Refresh token reuse detected'); + } + + const safeUser = await this.usersService.findByIdOrFail(decoded.sub); + if (safeUser.isDisabled) { + throw new ForbiddenException('Account is disabled'); + } + + await this.authRepository.revokeUserTokenByJti(decoded.sub, decoded.jti); + const authTokens = await this.generateAndStoreTokenPair( + decoded.sub, + safeUser.username, + safeUser.role, + ); + return { ...authTokens, user: safeUser.toObject() as unknown as Record }; + } + + async logout(dto: RefreshTokenDto): Promise { + try { + const decoded = this.jwtService.verify<{ sub: string }>(dto.refreshToken, { + secret: this.configService.get('jwt.refreshSecret', { infer: true }), + }); + await this.authRepository.revokeAllUserTokens(decoded.sub); + await this.authRepository.removeExpiredAndRevoked(decoded.sub); + } catch { + throw new BadRequestException('Invalid refresh token'); + } + } + + async loginWithGoogle(googleUser: { + googleId: string; + email: string; + name: string; + avatar?: string; + }): Promise { + let user = await this.usersService.findByGoogleId(googleUser.googleId); + + if (!user) { + user = await this.usersService.findByEmail(googleUser.email); + } + + if (!user) { + const generatedUsername = await this.generateUniqueUsernameFromEmail(googleUser.email); + const randomPassword = randomBytes(24).toString('hex'); + const saltRounds = this.configService.get('security.bcryptSaltRounds', { infer: true }); + const passwordHash = await hashValue(randomPassword, saltRounds); + + user = await this.usersService.create({ + name: googleUser.name, + username: generatedUsername, + email: googleUser.email, + password: passwordHash, + avatar: googleUser.avatar ?? '', + isVerified: true, + }); + } + + if (!user.googleId) { + user = await this.usersService.linkGoogleAccount(user.id, googleUser.googleId, googleUser.avatar); + } + + if (user.isDisabled) { + throw new ForbiddenException('Account is disabled'); + } + + const tokens = await this.generateAndStoreTokenPair(user.id, user.username, user.role ?? 'user'); + const safeUser = await this.usersService.findByIdOrFail(user.id); + return { ...tokens, user: safeUser.toObject() as unknown as Record }; + } + + async loginWithGoogleIdToken(dto: GoogleTokenLoginDto): Promise { + const clientId = this.configService.get('google.clientId', { infer: true }); + if (!clientId) { + throw new BadRequestException('Google client is not configured'); + } + + let payload: + | { + sub?: string; + email?: string; + email_verified?: boolean; + name?: string; + picture?: string; + } + | undefined; + try { + const ticket = await this.googleOAuthClient.verifyIdToken({ + idToken: dto.idToken, + audience: clientId, + }); + payload = ticket.getPayload(); + } catch { + throw new UnauthorizedException('Invalid Google id token'); + } + + if (!payload?.sub || !payload.email || payload.email_verified !== true) { + throw new UnauthorizedException('Google account data is invalid'); + } + + return this.loginWithGoogle({ + googleId: payload.sub, + email: payload.email.toLowerCase(), + name: payload.name ?? payload.email.split('@')[0], + avatar: payload.picture, + }); + } + + async superAdminLogin(dto: SuperAdminLoginDto): Promise<{ + accessToken: string; + refreshToken: string; + superAdmin: { email: string }; + }> { + const configuredEmail = this.configService.get('superAdmin.email', { infer: true }); + const configuredPassword = this.configService.get('superAdmin.password', { infer: true }); + + if ( + !configuredEmail || + !configuredPassword || + dto.email.toLowerCase() !== configuredEmail.toLowerCase() || + dto.password !== configuredPassword + ) { + throw new UnauthorizedException('Invalid superadmin credentials'); + } + + const tokens = await this.generateAndStoreSuperAdminTokenPair(configuredEmail); + return { ...tokens, superAdmin: { email: configuredEmail } }; + } + + async superAdminRefresh(dto: RefreshTokenDto): Promise<{ + accessToken: string; + refreshToken: string; + superAdmin: { email: string }; + }> { + const decoded = this.jwtService.verify<{ email: string; tokenType: string }>(dto.refreshToken, { + secret: this.configService.get('superAdmin.refreshSecret', { infer: true }), + }); + + if (decoded.tokenType !== 'superadmin_refresh' || !decoded.email) { + throw new UnauthorizedException('Invalid superadmin refresh token'); + } + + const activeTokens = await this.authRepository.findActiveSuperAdminTokens(decoded.email); + if (!activeTokens.length) { + throw new UnauthorizedException('Invalid superadmin refresh token'); + } + + let validTokenFound = false; + for (const token of activeTokens) { + const isMatch = await compareHash(dto.refreshToken, token.tokenHash); + if (isMatch) { + validTokenFound = true; + break; + } + } + + if (!validTokenFound) { + throw new UnauthorizedException('Invalid superadmin refresh token'); + } + + await this.authRepository.revokeAllSuperAdminTokens(decoded.email); + const tokens = await this.generateAndStoreSuperAdminTokenPair(decoded.email); + return { ...tokens, superAdmin: { email: decoded.email } }; + } + + async superAdminLogout(dto: RefreshTokenDto): Promise { + try { + const decoded = this.jwtService.verify<{ email: string }>(dto.refreshToken, { + secret: this.configService.get('superAdmin.refreshSecret', { infer: true }), + }); + await this.authRepository.revokeAllSuperAdminTokens(decoded.email); + await this.authRepository.removeExpiredAndRevokedSuperAdmin(decoded.email); + } catch { + throw new BadRequestException('Invalid superadmin refresh token'); + } + } + + async listUserSessions(userId: string): Promise<{ items: Array<{ jti: string; createdAt: Date; expiresAt: Date }> }> { + const sessions = await this.authRepository.listUserSessions(userId); + return { + items: sessions.map((s) => ({ + jti: s.jti, + createdAt: (s as unknown as { createdAt: Date }).createdAt, + expiresAt: s.expiresAt, + })), + }; + } + + async revokeUserSession(userId: string, jti: string): Promise { + await this.authRepository.revokeUserTokenByJti(userId, jti); + } + + async forgotPassword(dto: ForgotPasswordDto): Promise<{ message: string; debugCode?: string }> { + const normalizedEmail = dto.email.toLowerCase(); + const user = await this.usersService.findByEmail(normalizedEmail); + const message = 'If this email exists, a reset code was sent'; + + if (!user || user.isDisabled) { + return { message }; + } + + const code = this.generateResetCode(); + const saltRounds = this.configService.get('security.bcryptSaltRounds', { infer: true }); + const codeHash = await hashValue(code, saltRounds); + const expiresMinutes = this.configService.get('passwordReset.codeExpiresMinutes', { + infer: true, + }); + const expiresAt = new Date(Date.now() + expiresMinutes * 60 * 1000); + + await this.authRepository.invalidateActivePasswordResetCodes(user.id); + await this.authRepository.createPasswordResetCode(user.id, codeHash, expiresAt); + + await this.emailService.sendPasswordResetCode(normalizedEmail, code, expiresMinutes); + this.logger.log(`Password reset code generated for ${normalizedEmail}`); + + const nodeEnv = this.configService.get('nodeEnv', { infer: true }); + if (nodeEnv !== 'production') { + return { message, debugCode: code }; + } + + return { message }; + } + + async verifyResetCode( + dto: VerifyResetCodeDto, + ): Promise<{ resetToken: string; expiresIn: string }> { + const normalizedEmail = dto.email.toLowerCase(); + const user = await this.usersService.findByEmail(normalizedEmail); + if (!user || user.isDisabled) { + throw new UnauthorizedException('Invalid or expired reset code'); + } + + const codeRecord = await this.authRepository.findLatestActivePasswordResetCode(user.id); + if (!codeRecord) { + throw new UnauthorizedException('Invalid or expired reset code'); + } + + const maxAttempts = this.configService.get('passwordReset.maxAttempts', { infer: true }); + if (codeRecord.attempts >= maxAttempts) { + await this.authRepository.markPasswordResetCodeUsed(codeRecord.id); + throw new UnauthorizedException('Reset code attempts exceeded'); + } + + const isMatch = await compareHash(dto.code, codeRecord.codeHash); + if (!isMatch) { + await this.authRepository.incrementPasswordResetAttempts(codeRecord.id); + const attemptsAfter = codeRecord.attempts + 1; + if (attemptsAfter >= maxAttempts) { + await this.authRepository.markPasswordResetCodeUsed(codeRecord.id); + } + throw new UnauthorizedException('Invalid or expired reset code'); + } + + await this.authRepository.markPasswordResetCodeVerified(codeRecord.id); + const resetTokenExpiresIn = + this.configService.get('passwordReset.tokenExpiresIn', { + infer: true, + }) ?? '15m'; + const resetToken = await this.jwtService.signAsync( + { sub: user.id, tokenType: 'password_reset', prcId: codeRecord.id }, + { + secret: this.configService.get('passwordReset.tokenSecret', { infer: true }), + expiresIn: resetTokenExpiresIn, + }, + ); + + return { + resetToken, + expiresIn: resetTokenExpiresIn, + }; + } + + async resetPassword(dto: ResetPasswordDto): Promise<{ message: string }> { + if (dto.newPassword !== dto.confirmPassword) { + throw new BadRequestException('Password confirmation does not match'); + } + + let decoded: { sub: string; tokenType: string; prcId: string }; + try { + decoded = this.jwtService.verify(dto.resetToken, { + secret: this.configService.get('passwordReset.tokenSecret', { infer: true }), + }); + } catch { + throw new UnauthorizedException('Invalid or expired reset token'); + } + + if (decoded.tokenType !== 'password_reset' || !decoded.prcId || !decoded.sub) { + throw new UnauthorizedException('Invalid or expired reset token'); + } + + const codeRecord = await this.authRepository.findValidVerifiedPasswordResetCode( + decoded.prcId, + decoded.sub, + ); + if (!codeRecord) { + throw new UnauthorizedException('Invalid or expired reset token'); + } + + const user = await this.usersService.findByIdOrFail(decoded.sub); + if (user.isDisabled) { + throw new ForbiddenException('Account is disabled'); + } + + const saltRounds = this.configService.get('security.bcryptSaltRounds', { infer: true }); + const passwordHash = await hashValue(dto.newPassword, saltRounds); + await this.usersService.updatePassword(decoded.sub, passwordHash); + + await this.authRepository.markPasswordResetCodeUsed(codeRecord.id); + await this.authRepository.markPasswordResetCodeUsedByUser(decoded.sub); + await this.authRepository.revokeAllUserTokens(decoded.sub); + await this.authRepository.removeExpiredAndRevoked(decoded.sub); + + return { message: 'Password reset successfully' }; + } + + private async generateAndStoreTokenPair( + userId: string, + username: string, + role: string, + ): Promise { + const refreshJti = randomUUID(); + const [accessToken, refreshToken] = await Promise.all([ + this.jwtService.signAsync( + { sub: userId, username, role, tokenType: 'access' }, + { + secret: this.configService.get('jwt.accessSecret', { infer: true }), + expiresIn: this.configService.get('jwt.accessExpiresIn', { infer: true }), + }, + ), + this.jwtService.signAsync( + { sub: userId, username, role, tokenType: 'refresh', jti: refreshJti }, + { + secret: this.configService.get('jwt.refreshSecret', { infer: true }), + expiresIn: this.configService.get('jwt.refreshExpiresIn', { infer: true }), + }, + ), + ]); + + const saltRounds = this.configService.get('security.bcryptSaltRounds', { infer: true }); + const tokenHash = await hashValue(refreshToken, saltRounds); + + const refreshExpiresIn = this.configService.get('jwt.refreshExpiresIn', { + infer: true, + }); + const refreshExpiresInMs = this.parseExpiresInToMs(refreshExpiresIn ?? '30d'); + + await this.authRepository.createRefreshToken(userId, refreshJti, tokenHash, new Date(Date.now() + refreshExpiresInMs)); + + return { accessToken, refreshToken }; + } + + private async generateAndStoreSuperAdminTokenPair(adminEmail: string): Promise { + const [accessToken, refreshToken] = await Promise.all([ + this.jwtService.signAsync( + { + sub: 'superadmin', + username: 'superadmin', + email: adminEmail.toLowerCase(), + role: 'superadmin', + tokenType: 'superadmin_access', + }, + { + secret: this.configService.get('superAdmin.accessSecret', { infer: true }), + expiresIn: this.configService.get('superAdmin.accessExpiresIn', { infer: true }), + }, + ), + this.jwtService.signAsync( + { + sub: 'superadmin', + username: 'superadmin', + email: adminEmail.toLowerCase(), + role: 'superadmin', + tokenType: 'superadmin_refresh', + }, + { + secret: this.configService.get('superAdmin.refreshSecret', { infer: true }), + expiresIn: this.configService.get('superAdmin.refreshExpiresIn', { infer: true }), + }, + ), + ]); + + const saltRounds = this.configService.get('security.bcryptSaltRounds', { infer: true }); + const tokenHash = await hashValue(refreshToken, saltRounds); + const refreshExpiresIn = this.configService.get('superAdmin.refreshExpiresIn', { + infer: true, + }); + const refreshExpiresInMs = this.parseExpiresInToMs(refreshExpiresIn ?? '30d'); + + await this.authRepository.createSuperAdminRefreshToken( + adminEmail, + tokenHash, + new Date(Date.now() + refreshExpiresInMs), + ); + + return { accessToken, refreshToken }; + } + + private parseExpiresInToMs(expiresIn: string): number { + const regex = /^(\d+)([smhd])$/; + const match = expiresIn.match(regex); + if (!match) { + return 30 * 24 * 60 * 60 * 1000; + } + + const value = Number(match[1]); + const unit = match[2]; + + const multipliers: Record = { + s: 1000, + m: 60 * 1000, + h: 60 * 60 * 1000, + d: 24 * 60 * 60 * 1000, + }; + + return value * multipliers[unit]; + } + + private async generateUniqueUsernameFromEmail(email: string): Promise { + const base = email.split('@')[0].replace(/[^a-zA-Z0-9_.]/g, '').toLowerCase() || 'user'; + let candidate = base.slice(0, 24); + let counter = 1; + + while (await this.usersService.findByUsername(candidate)) { + const suffix = `_${counter}`; + const maxBaseLength = 30 - suffix.length; + candidate = `${base.slice(0, Math.max(1, maxBaseLength))}${suffix}`; + counter += 1; + } + + return candidate; + } + + private generateResetCode(): string { + return String(randomInt(100000, 1000000)); + } + + private async issueEmailVerificationCode(userId: string, email: string): Promise { + const code = this.generateResetCode(); + const saltRounds = this.configService.get('security.bcryptSaltRounds', { infer: true }); + const codeHash = await hashValue(code, saltRounds); + const expiresMinutes = this.configService.get('emailVerification.codeExpiresMinutes', { + infer: true, + }); + const expiresAt = new Date(Date.now() + expiresMinutes * 60 * 1000); + + await this.authRepository.invalidateActiveEmailVerificationCodes(userId); + await this.authRepository.createEmailVerificationCode(userId, codeHash, expiresAt); + + await this.emailService.sendVerificationCode(email, code, expiresMinutes); + this.logger.log(`Email verification code generated for ${email}`); + return code; + } +} diff --git a/src/modules/auth/dto/auth-response.dto.ts b/src/modules/auth/dto/auth-response.dto.ts new file mode 100644 index 0000000..a9fd5ff --- /dev/null +++ b/src/modules/auth/dto/auth-response.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class AuthResponseDto { + @ApiProperty() + accessToken!: string; + + @ApiProperty() + refreshToken!: string; + + @ApiProperty({ + description: 'Full user profile without password', + type: 'object', + additionalProperties: true, + }) + user!: Record; +} diff --git a/src/modules/auth/dto/forgot-password.dto.ts b/src/modules/auth/dto/forgot-password.dto.ts new file mode 100644 index 0000000..ee20436 --- /dev/null +++ b/src/modules/auth/dto/forgot-password.dto.ts @@ -0,0 +1,8 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail } from 'class-validator'; + +export class ForgotPasswordDto { + @ApiProperty({ example: 'user@example.com' }) + @IsEmail() + email!: string; +} diff --git a/src/modules/auth/dto/google-token-login.dto.ts b/src/modules/auth/dto/google-token-login.dto.ts new file mode 100644 index 0000000..80d605d --- /dev/null +++ b/src/modules/auth/dto/google-token-login.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, MinLength } from 'class-validator'; + +export class GoogleTokenLoginDto { + @ApiProperty({ description: 'Google ID token from frontend Google Sign-In' }) + @IsString() + @MinLength(20) + idToken!: string; +} diff --git a/src/modules/auth/dto/login.dto.ts b/src/modules/auth/dto/login.dto.ts new file mode 100644 index 0000000..8cb03b6 --- /dev/null +++ b/src/modules/auth/dto/login.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsString, Length } from 'class-validator'; + +export class LoginDto { + @ApiProperty({ example: 'john@example.com' }) + @IsEmail() + email!: string; + + @ApiProperty({ minLength: 8 }) + @IsString() + @Length(8, 64) + password!: string; +} diff --git a/src/modules/auth/dto/refresh-token.dto.ts b/src/modules/auth/dto/refresh-token.dto.ts new file mode 100644 index 0000000..162791c --- /dev/null +++ b/src/modules/auth/dto/refresh-token.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, MinLength } from 'class-validator'; + +export class RefreshTokenDto { + @ApiProperty() + @IsString() + @MinLength(20) + refreshToken!: string; +} diff --git a/src/modules/auth/dto/register-basic.dto.ts b/src/modules/auth/dto/register-basic.dto.ts new file mode 100644 index 0000000..0d04981 --- /dev/null +++ b/src/modules/auth/dto/register-basic.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsString, Length } from 'class-validator'; + +export class RegisterBasicDto { + @ApiProperty({ example: 'user@example.com' }) + @IsEmail() + email!: string; + + @ApiProperty({ minLength: 8, example: 'StrongPass123!' }) + @IsString() + @Length(8, 64) + password!: string; + + @ApiProperty({ minLength: 8, example: 'StrongPass123!' }) + @IsString() + @Length(8, 64) + confirmPassword!: string; +} diff --git a/src/modules/auth/dto/register.dto.ts b/src/modules/auth/dto/register.dto.ts new file mode 100644 index 0000000..05c6016 --- /dev/null +++ b/src/modules/auth/dto/register.dto.ts @@ -0,0 +1,112 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + IsArray, + IsBoolean, + IsEmail, + IsEnum, + IsNumber, + IsOptional, + IsString, + Length, + Max, + Matches, + Min, +} from 'class-validator'; +import { ExperienceLevel } from '../../../common/enums/experience-level.enum'; +import { MusicRole } from '../../../common/enums/music-role.enum'; + +export class RegisterDto { + @ApiProperty({ example: 'john@example.com' }) + @IsEmail() + email!: string; + + @ApiProperty({ minLength: 8 }) + @IsString() + @Length(8, 64) + password!: string; + + @ApiProperty({ minLength: 8 }) + @IsString() + @Length(8, 64) + confirmPassword!: string; + + @ApiProperty({ required: false, example: 'John Doe' }) + @IsOptional() + @IsString() + @Length(2, 80) + name?: string; + + @ApiProperty({ required: false, example: 'john_doe' }) + @IsOptional() + @IsString() + @Length(3, 30) + @Matches(/^[a-zA-Z0-9_.]+$/, { message: 'username can contain letters, numbers, _ and .' }) + username?: string; + + @ApiProperty({ required: false, example: 'Artist One' }) + @IsOptional() + @IsString() + @Length(0, 80) + stageName?: string; + + @ApiProperty({ required: false, maxLength: 160 }) + @IsOptional() + @IsString() + @Length(0, 160) + bio?: string; + + @ApiProperty({ required: false, example: 'Riyadh, Saudi Arabia' }) + @IsOptional() + @IsString() + @Length(0, 120) + location?: string; + + @ApiProperty({ example: 24.7136, minimum: -90, maximum: 90 }) + @Type(() => Number) + @IsNumber() + @Min(-90) + @Max(90) + latitude!: number; + + @ApiProperty({ example: 46.6753, minimum: -180, maximum: 180 }) + @Type(() => Number) + @IsNumber() + @Min(-180) + @Max(180) + longitude!: number; + + @ApiProperty({ required: false, default: false }) + @IsOptional() + @IsBoolean() + isPrivate?: boolean; + + @ApiProperty({ required: false, enum: MusicRole, isArray: true }) + @IsOptional() + @IsArray() + @IsEnum(MusicRole, { each: true }) + musicRoles?: MusicRole[]; + + @ApiProperty({ required: false, enum: ExperienceLevel, example: ExperienceLevel.BEGINNER }) + @IsOptional() + @IsEnum(ExperienceLevel) + experienceLevel?: ExperienceLevel; + + @ApiProperty({ required: false, type: [String], example: ['Tarab', 'Pop'] }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + musicGenres?: string[]; + + @ApiProperty({ required: false, type: [String], example: ['Oud', 'Piano'] }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + favoriteInstruments?: string[]; + + @ApiProperty({ required: false, type: [String], example: ['Bayati', 'Rast'] }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + favoriteMaqamat?: string[]; +} diff --git a/src/modules/auth/dto/reset-password.dto.ts b/src/modules/auth/dto/reset-password.dto.ts new file mode 100644 index 0000000..d1a9e8b --- /dev/null +++ b/src/modules/auth/dto/reset-password.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, Length } from 'class-validator'; + +export class ResetPasswordDto { + @ApiProperty() + @IsString() + resetToken!: string; + + @ApiProperty({ minLength: 8, example: 'NewStrongPass123!' }) + @IsString() + @Length(8, 64) + newPassword!: string; + + @ApiProperty({ minLength: 8, example: 'NewStrongPass123!' }) + @IsString() + @Length(8, 64) + confirmPassword!: string; +} diff --git a/src/modules/auth/dto/send-email-verification.dto.ts b/src/modules/auth/dto/send-email-verification.dto.ts new file mode 100644 index 0000000..297514c --- /dev/null +++ b/src/modules/auth/dto/send-email-verification.dto.ts @@ -0,0 +1,8 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail } from 'class-validator'; + +export class SendEmailVerificationDto { + @ApiProperty({ example: 'user@example.com' }) + @IsEmail() + email!: string; +} diff --git a/src/modules/auth/dto/super-admin-login.dto.ts b/src/modules/auth/dto/super-admin-login.dto.ts new file mode 100644 index 0000000..ebb7ba5 --- /dev/null +++ b/src/modules/auth/dto/super-admin-login.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsString, Length } from 'class-validator'; + +export class SuperAdminLoginDto { + @ApiProperty({ example: 'admin@oudelaa.com' }) + @IsEmail() + email!: string; + + @ApiProperty({ example: 'SuperAdminStrongPass123!' }) + @IsString() + @Length(8, 128) + password!: string; +} diff --git a/src/modules/auth/dto/verify-email.dto.ts b/src/modules/auth/dto/verify-email.dto.ts new file mode 100644 index 0000000..f92bc1a --- /dev/null +++ b/src/modules/auth/dto/verify-email.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsString, Length, Matches } from 'class-validator'; + +export class VerifyEmailDto { + @ApiProperty({ example: 'user@example.com' }) + @IsEmail() + email!: string; + + @ApiProperty({ example: '123456' }) + @IsString() + @Length(6, 6) + @Matches(/^\d{6}$/, { message: 'code must be 6 digits' }) + code!: string; +} diff --git a/src/modules/auth/dto/verify-reset-code.dto.ts b/src/modules/auth/dto/verify-reset-code.dto.ts new file mode 100644 index 0000000..67b0171 --- /dev/null +++ b/src/modules/auth/dto/verify-reset-code.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsString, Length, Matches } from 'class-validator'; + +export class VerifyResetCodeDto { + @ApiProperty({ example: 'user@example.com' }) + @IsEmail() + email!: string; + + @ApiProperty({ example: '123456' }) + @IsString() + @Length(6, 6) + @Matches(/^\d{6}$/, { message: 'code must be 6 digits' }) + code!: string; +} diff --git a/src/modules/auth/guards/google-auth.guard.ts b/src/modules/auth/guards/google-auth.guard.ts new file mode 100644 index 0000000..4a2c87a --- /dev/null +++ b/src/modules/auth/guards/google-auth.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class GoogleAuthGuard extends AuthGuard('google') {} diff --git a/src/modules/auth/schemas/email-verification-code.schema.ts b/src/modules/auth/schemas/email-verification-code.schema.ts new file mode 100644 index 0000000..8ea7ebd --- /dev/null +++ b/src/modules/auth/schemas/email-verification-code.schema.ts @@ -0,0 +1,28 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { HydratedDocument, Types } from 'mongoose'; +import { User } from '../../users/schemas/user.schema'; + +export type EmailVerificationCodeDocument = HydratedDocument; + +@Schema({ timestamps: true, versionKey: false }) +export class EmailVerificationCode { + @Prop({ type: Types.ObjectId, ref: User.name, required: true, index: true }) + userId!: Types.ObjectId; + + @Prop({ required: true, select: false }) + codeHash!: string; + + @Prop({ required: true, index: true }) + expiresAt!: Date; + + @Prop({ default: 0, min: 0 }) + attempts!: number; + + @Prop({ default: false, index: true }) + used!: boolean; +} + +export const EmailVerificationCodeSchema = SchemaFactory.createForClass(EmailVerificationCode); + +EmailVerificationCodeSchema.index({ userId: 1, used: 1, expiresAt: -1 }); +EmailVerificationCodeSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 }); diff --git a/src/modules/auth/schemas/password-reset-code.schema.ts b/src/modules/auth/schemas/password-reset-code.schema.ts new file mode 100644 index 0000000..616e37e --- /dev/null +++ b/src/modules/auth/schemas/password-reset-code.schema.ts @@ -0,0 +1,31 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { HydratedDocument, Types } from 'mongoose'; +import { User } from '../../users/schemas/user.schema'; + +export type PasswordResetCodeDocument = HydratedDocument; + +@Schema({ timestamps: true, versionKey: false }) +export class PasswordResetCode { + @Prop({ type: Types.ObjectId, ref: User.name, required: true, index: true }) + userId!: Types.ObjectId; + + @Prop({ required: true, select: false }) + codeHash!: string; + + @Prop({ required: true, index: true }) + expiresAt!: Date; + + @Prop({ default: 0, min: 0 }) + attempts!: number; + + @Prop({ default: false, index: true }) + verified!: boolean; + + @Prop({ default: false, index: true }) + used!: boolean; +} + +export const PasswordResetCodeSchema = SchemaFactory.createForClass(PasswordResetCode); + +PasswordResetCodeSchema.index({ userId: 1, used: 1, expiresAt: -1 }); +PasswordResetCodeSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 }); diff --git a/src/modules/auth/schemas/refresh-token.schema.ts b/src/modules/auth/schemas/refresh-token.schema.ts new file mode 100644 index 0000000..b83601c --- /dev/null +++ b/src/modules/auth/schemas/refresh-token.schema.ts @@ -0,0 +1,32 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { HydratedDocument, Types } from 'mongoose'; +import { User } from '../../users/schemas/user.schema'; + +export type RefreshTokenDocument = HydratedDocument; + +@Schema({ timestamps: true, versionKey: false }) +export class RefreshToken { + @Prop({ type: Types.ObjectId, ref: User.name, required: true, index: true }) + userId!: Types.ObjectId; + + @Prop({ required: true, select: false }) + tokenHash!: string; + + @Prop({ required: true, index: true, unique: true }) + jti!: string; + + @Prop({ required: true }) + expiresAt!: Date; + + @Prop({ default: false }) + revoked!: boolean; + + @Prop({ default: false, index: true }) + compromised!: boolean; +} + +export const RefreshTokenSchema = SchemaFactory.createForClass(RefreshToken); + +RefreshTokenSchema.index({ userId: 1, revoked: 1 }); +RefreshTokenSchema.index({ userId: 1, jti: 1 }); +RefreshTokenSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 }); diff --git a/src/modules/auth/schemas/super-admin-refresh-token.schema.ts b/src/modules/auth/schemas/super-admin-refresh-token.schema.ts new file mode 100644 index 0000000..f3336bb --- /dev/null +++ b/src/modules/auth/schemas/super-admin-refresh-token.schema.ts @@ -0,0 +1,23 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { HydratedDocument } from 'mongoose'; + +export type SuperAdminRefreshTokenDocument = HydratedDocument; + +@Schema({ timestamps: true, versionKey: false }) +export class SuperAdminRefreshToken { + @Prop({ required: true, trim: true, lowercase: true, index: true }) + adminEmail!: string; + + @Prop({ required: true, select: false }) + tokenHash!: string; + + @Prop({ required: true }) + expiresAt!: Date; + + @Prop({ default: false }) + revoked!: boolean; +} + +export const SuperAdminRefreshTokenSchema = SchemaFactory.createForClass(SuperAdminRefreshToken); +SuperAdminRefreshTokenSchema.index({ adminEmail: 1, revoked: 1 }); +SuperAdminRefreshTokenSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 }); diff --git a/src/modules/auth/strategies/google.strategy.ts b/src/modules/auth/strategies/google.strategy.ts new file mode 100644 index 0000000..4a3e5b1 --- /dev/null +++ b/src/modules/auth/strategies/google.strategy.ts @@ -0,0 +1,42 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { PassportStrategy } from '@nestjs/passport'; +import { Profile, Strategy, VerifyCallback } from 'passport-google-oauth20'; + +@Injectable() +export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { + constructor(configService: ConfigService) { + super({ + clientID: + configService.get('google.clientId', { infer: true }) || 'missing-google-client-id', + clientSecret: + configService.get('google.clientSecret', { infer: true }) || + 'missing-google-client-secret', + callbackURL: + configService.get('google.callbackUrl', { infer: true }) || + 'http://localhost:4000/api/v1/auth/google/callback', + scope: ['email', 'profile'], + }); + } + + validate( + _accessToken: string, + _refreshToken: string, + profile: Profile, + done: VerifyCallback, + ): void { + const email = profile.emails?.[0]?.value?.toLowerCase(); + + if (!email) { + done(new UnauthorizedException('Google account email is not available')); + return; + } + + done(null, { + googleId: profile.id, + email, + name: profile.displayName ?? 'Google User', + avatar: profile.photos?.[0]?.value, + }); + } +} diff --git a/src/modules/auth/strategies/jwt-refresh.strategy.ts b/src/modules/auth/strategies/jwt-refresh.strategy.ts new file mode 100644 index 0000000..176d3bf --- /dev/null +++ b/src/modules/auth/strategies/jwt-refresh.strategy.ts @@ -0,0 +1,26 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { PassportStrategy } from '@nestjs/passport'; +import { Request } from 'express'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { JwtPayload } from '../../../common/interfaces/jwt-payload.interface'; + +@Injectable() +export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt-refresh') { + constructor(configService: ConfigService) { + super({ + jwtFromRequest: ExtractJwt.fromBodyField('refreshToken'), + ignoreExpiration: false, + secretOrKey: configService.get('jwt.refreshSecret', { infer: true }), + passReqToCallback: true, + }); + } + + validate(_: Request, payload: JwtPayload): JwtPayload { + if (payload.tokenType !== 'refresh') { + throw new UnauthorizedException('Invalid token type'); + } + + return payload; + } +} diff --git a/src/modules/auth/strategies/jwt.strategy.ts b/src/modules/auth/strategies/jwt.strategy.ts new file mode 100644 index 0000000..100d9bf --- /dev/null +++ b/src/modules/auth/strategies/jwt.strategy.ts @@ -0,0 +1,24 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { JwtPayload } from '../../../common/interfaces/jwt-payload.interface'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { + constructor(configService: ConfigService) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: configService.get('jwt.accessSecret', { infer: true }), + }); + } + + validate(payload: JwtPayload): JwtPayload { + if (payload.tokenType !== 'access') { + throw new UnauthorizedException('Invalid token type'); + } + + return payload; + } +} diff --git a/src/modules/auth/strategies/super-admin-jwt.strategy.ts b/src/modules/auth/strategies/super-admin-jwt.strategy.ts new file mode 100644 index 0000000..090686b --- /dev/null +++ b/src/modules/auth/strategies/super-admin-jwt.strategy.ts @@ -0,0 +1,24 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { JwtPayload } from '../../../common/interfaces/jwt-payload.interface'; + +@Injectable() +export class SuperAdminJwtStrategy extends PassportStrategy(Strategy, 'superadmin-jwt') { + constructor(configService: ConfigService) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: configService.get('superAdmin.accessSecret', { infer: true }), + }); + } + + validate(payload: JwtPayload): JwtPayload { + if (payload.tokenType !== 'superadmin_access') { + throw new UnauthorizedException('Invalid superadmin token'); + } + + return payload; + } +} diff --git a/src/modules/auth/types/token-pair.type.ts b/src/modules/auth/types/token-pair.type.ts new file mode 100644 index 0000000..d7d11cd --- /dev/null +++ b/src/modules/auth/types/token-pair.type.ts @@ -0,0 +1,8 @@ +export type TokenPair = { + accessToken: string; + refreshToken: string; +}; + +export type AuthResult = TokenPair & { + user: Record; +}; diff --git a/src/modules/chat/chat.controller.ts b/src/modules/chat/chat.controller.ts new file mode 100644 index 0000000..3880bac --- /dev/null +++ b/src/modules/chat/chat.controller.ts @@ -0,0 +1,78 @@ +import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { CurrentUser } from '../../common/decorators/current-user.decorator'; +import { Throttle } from '../../common/decorators/throttle.decorator'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { JwtPayload } from '../../common/interfaces/jwt-payload.interface'; +import { ChatService } from './chat.service'; +import { CreateConversationDto } from './dto/create-conversation.dto'; +import { MessageQueryDto } from './dto/message-query.dto'; +import { SendMessageDto } from './dto/send-message.dto'; + +@ApiTags('Chat') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard) +@Controller('chat') +export class ChatController { + constructor(private readonly chatService: ChatService) {} + + @Post('conversations') + @Throttle(40, 60_000) + async createConversation(@CurrentUser() user: JwtPayload, @Body() dto: CreateConversationDto) { + return this.chatService.createConversation(user.sub, dto); + } + + @Get('conversations') + async myConversations(@CurrentUser() user: JwtPayload, @Query() query: MessageQueryDto) { + return this.chatService.getMyConversations(user.sub, query); + } + + @Get('conversations/:conversationId/messages') + async messages( + @CurrentUser() user: JwtPayload, + @Param('conversationId') conversationId: string, + @Query() query: MessageQueryDto, + ) { + return this.chatService.getMessages(user.sub, conversationId, query); + } + + @Post('messages') + @Throttle(120, 60_000) + async sendMessage(@CurrentUser() user: JwtPayload, @Body() dto: SendMessageDto) { + return this.chatService.sendMessage(user.sub, dto); + } + + @Patch('messages/:messageId/seen') + @Throttle(200, 60_000) + async markSeen(@CurrentUser() user: JwtPayload, @Param('messageId') messageId: string) { + return this.chatService.markMessageSeen(user.sub, messageId); + } + + @Patch('messages/:messageId/unsend') + @Throttle(80, 60_000) + async unsend(@CurrentUser() user: JwtPayload, @Param('messageId') messageId: string) { + return this.chatService.unsendMessage(user.sub, messageId); + } + + @Post('blocks/:targetUserId') + @Throttle(20, 60_000) + async blockUser(@CurrentUser() user: JwtPayload, @Param('targetUserId') targetUserId: string) { + return this.chatService.blockUser(user.sub, targetUserId); + } + + @Patch('blocks/:targetUserId/unblock') + @Throttle(20, 60_000) + async unblockUser(@CurrentUser() user: JwtPayload, @Param('targetUserId') targetUserId: string) { + return this.chatService.unblockUser(user.sub, targetUserId); + } + + @Get('blocks/status/:targetUserId') + async blockStatus(@CurrentUser() user: JwtPayload, @Param('targetUserId') targetUserId: string) { + return this.chatService.getBlockStatus(user.sub, targetUserId); + } + + @Get('blocks') + async myBlocks(@CurrentUser() user: JwtPayload) { + return this.chatService.getMyBlockedUsers(user.sub); + } +} diff --git a/src/modules/chat/chat.gateway.ts b/src/modules/chat/chat.gateway.ts new file mode 100644 index 0000000..7193755 --- /dev/null +++ b/src/modules/chat/chat.gateway.ts @@ -0,0 +1,137 @@ +import { ConfigService } from '@nestjs/config'; +import { JwtService } from '@nestjs/jwt'; +import { + ConnectedSocket, + MessageBody, + OnGatewayConnection, + OnGatewayDisconnect, + SubscribeMessage, + WebSocketGateway, + WebSocketServer, +} from '@nestjs/websockets'; +import { Server, Socket } from 'socket.io'; +import { ChatService } from './chat.service'; +import { SendMessageDto } from './dto/send-message.dto'; + +type SocketWithUser = Socket & { data: { userId?: string } }; + +@WebSocketGateway({ cors: { origin: '*' }, namespace: 'chat' }) +export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { + @WebSocketServer() + server!: Server; + + constructor( + private readonly chatService: ChatService, + private readonly jwtService: JwtService, + private readonly configService: ConfigService, + ) {} + + async handleConnection(client: SocketWithUser) { + const token = this.extractToken(client); + if (!token) { + client.disconnect(true); + return; + } + + try { + const payload = this.jwtService.verify<{ sub: string; tokenType: string }>(token, { + secret: this.configService.get('jwt.accessSecret', { infer: true }), + }); + if (payload.tokenType !== 'access') { + client.disconnect(true); + return; + } + client.data.userId = payload.sub; + await client.join(this.userRoom(payload.sub)); + this.server.to(this.userRoom(payload.sub)).emit('presence', { userId: payload.sub, online: true }); + } catch { + client.disconnect(true); + } + } + + handleDisconnect(client: SocketWithUser) { + const userId = client.data.userId; + if (userId) { + this.server.to(this.userRoom(userId)).emit('presence', { userId, online: false }); + } + } + + @SubscribeMessage('join_conversation') + async joinConversation( + @ConnectedSocket() client: SocketWithUser, + @MessageBody() body: { conversationId: string }, + ) { + const userId = client.data.userId; + if (!userId) return; + + const conversation = await this.chatService.assertConversationMember(userId, body.conversationId); + await client.join(this.conversationRoom(conversation.id)); + client.emit('joined_conversation', { conversationId: conversation.id }); + } + + @SubscribeMessage('send_message') + async sendMessage( + @ConnectedSocket() client: SocketWithUser, + @MessageBody() dto: SendMessageDto, + ) { + const userId = client.data.userId; + if (!userId) return; + + const message = await this.chatService.sendMessage(userId, dto); + this.server.to(this.conversationRoom(message.conversationId.toString())).emit('new_message', message); + return message; + } + + @SubscribeMessage('typing') + async typing( + @ConnectedSocket() client: SocketWithUser, + @MessageBody() body: { conversationId: string; isTyping: boolean }, + ) { + const userId = client.data.userId; + if (!userId) return; + + await this.chatService.assertConversationMember(userId, body.conversationId); + client.to(this.conversationRoom(body.conversationId)).emit('typing', { + conversationId: body.conversationId, + userId, + isTyping: !!body.isTyping, + }); + } + + @SubscribeMessage('mark_seen') + async markSeen( + @ConnectedSocket() client: SocketWithUser, + @MessageBody() body: { messageId: string; conversationId: string }, + ) { + const userId = client.data.userId; + if (!userId) return; + + await this.chatService.markMessageSeen(userId, body.messageId); + this.server.to(this.conversationRoom(body.conversationId)).emit('message_seen', { + messageId: body.messageId, + userId, + }); + } + + private extractToken(client: Socket): string | null { + const authToken = client.handshake.auth?.token; + if (typeof authToken === 'string' && authToken.trim()) { + return authToken.replace(/^Bearer\s+/i, '').trim(); + } + + const headerAuth = client.handshake.headers.authorization; + if (typeof headerAuth === 'string' && headerAuth.trim()) { + return headerAuth.replace(/^Bearer\s+/i, '').trim(); + } + + return null; + } + + private userRoom(userId: string): string { + return `user:${userId}`; + } + + private conversationRoom(conversationId: string): string { + return `conversation:${conversationId}`; + } +} diff --git a/src/modules/chat/chat.module.ts b/src/modules/chat/chat.module.ts new file mode 100644 index 0000000..00108cb --- /dev/null +++ b/src/modules/chat/chat.module.ts @@ -0,0 +1,29 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { JwtModule } from '@nestjs/jwt'; +import { MongooseModule } from '@nestjs/mongoose'; +import { UsersModule } from '../users/users.module'; +import { ChatController } from './chat.controller'; +import { ChatGateway } from './chat.gateway'; +import { ChatService } from './chat.service'; +import { ChatRepository } from './chat.repository'; +import { ChatBlock, ChatBlockSchema } from './schemas/chat-block.schema'; +import { Conversation, ConversationSchema } from './schemas/conversation.schema'; +import { Message, MessageSchema } from './schemas/message.schema'; + +@Module({ + imports: [ + ConfigModule, + JwtModule.register({}), + UsersModule, + MongooseModule.forFeature([ + { name: Conversation.name, schema: ConversationSchema }, + { name: Message.name, schema: MessageSchema }, + { name: ChatBlock.name, schema: ChatBlockSchema }, + ]), + ], + controllers: [ChatController], + providers: [ChatService, ChatRepository, ChatGateway], + exports: [ChatService], +}) +export class ChatModule {} diff --git a/src/modules/chat/chat.repository.ts b/src/modules/chat/chat.repository.ts new file mode 100644 index 0000000..4eb322b --- /dev/null +++ b/src/modules/chat/chat.repository.ts @@ -0,0 +1,221 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { FilterQuery, Model, Types } from 'mongoose'; +import { ChatBlock, ChatBlockDocument } from './schemas/chat-block.schema'; +import { Conversation, ConversationDocument } from './schemas/conversation.schema'; +import { Message, MessageDocument } from './schemas/message.schema'; + +@Injectable() +export class ChatRepository { + constructor( + @InjectModel(Conversation.name) private readonly conversationModel: Model, + @InjectModel(Message.name) private readonly messageModel: Model, + @InjectModel(ChatBlock.name) private readonly chatBlockModel: Model, + ) {} + + async findConversationById(id: string): Promise { + return this.conversationModel.findById(id).exec(); + } + + async findDirectConversation(userAId: string, userBId: string): Promise { + return this.conversationModel + .findOne({ + isGroup: false, + participantIds: { + $all: [new Types.ObjectId(userAId), new Types.ObjectId(userBId)], + $size: 2, + }, + }) + .exec(); + } + + async createConversation(payload: { + participantIds: string[]; + isGroup: boolean; + title?: string; + createdBy: string; + }): Promise { + const participantIds = payload.participantIds.map((id) => new Types.ObjectId(id)); + const unreadCountByUser: Record = {}; + payload.participantIds.forEach((id) => { + unreadCountByUser[id] = 0; + }); + + return this.conversationModel.create({ + participantIds, + isGroup: payload.isGroup, + title: payload.title ?? '', + createdBy: new Types.ObjectId(payload.createdBy), + unreadCountByUser, + lastMessageText: '', + }); + } + + async findConversationsForUser(userId: string, skip: number, limit: number): Promise { + return this.conversationModel + .find({ participantIds: new Types.ObjectId(userId) }) + .populate({ path: 'participantIds', select: 'name username stageName avatar isVerified isDisabled' }) + .sort({ lastMessageAt: -1, updatedAt: -1 }) + .skip(skip) + .limit(limit) + .exec(); + } + + async countConversationsForUser(userId: string): Promise { + return this.conversationModel.countDocuments({ participantIds: new Types.ObjectId(userId) }).exec(); + } + + async createMessage(payload: { + conversationId: string; + senderId: string; + content?: string; + messageType: 'text' | 'image' | 'video' | 'audio'; + mediaUrl?: string; + }): Promise { + return this.messageModel.create({ + conversationId: new Types.ObjectId(payload.conversationId), + senderId: new Types.ObjectId(payload.senderId), + content: payload.content ?? '', + messageType: payload.messageType, + mediaUrl: payload.mediaUrl ?? '', + seenBy: [new Types.ObjectId(payload.senderId)], + isUnsent: false, + }); + } + + async findMessages(conversationId: string, skip: number, limit: number): Promise { + return this.messageModel + .find({ conversationId: new Types.ObjectId(conversationId) }) + .populate({ path: 'senderId', select: 'name username stageName avatar isVerified' }) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit) + .exec(); + } + + async countMessages(conversationId: string): Promise { + return this.messageModel.countDocuments({ conversationId: new Types.ObjectId(conversationId) }).exec(); + } + + async findMessageById(messageId: string): Promise { + return this.messageModel.findById(messageId).exec(); + } + + async markMessageSeen(messageId: string, userId: string): Promise { + await this.messageModel + .findByIdAndUpdate(messageId, { $addToSet: { seenBy: new Types.ObjectId(userId) } }, { new: false }) + .exec(); + } + + async updateConversationAfterNewMessage( + conversationId: string, + messageId: string, + senderId: string, + messageText: string, + ): Promise { + const conversation = await this.conversationModel.findById(conversationId).exec(); + if (!conversation) { + return null; + } + + const unreadMap = new Map( + Object.entries((conversation.unreadCountByUser as unknown as Record) ?? {}), + ); + + for (const participantId of conversation.participantIds) { + const id = participantId.toString(); + if (id === senderId) { + unreadMap.set(id, 0); + } else { + unreadMap.set(id, (unreadMap.get(id) ?? 0) + 1); + } + } + + conversation.lastMessageId = new Types.ObjectId(messageId); + conversation.lastMessageText = messageText.slice(0, 4000); + conversation.lastMessageAt = new Date(); + conversation.unreadCountByUser = unreadMap as unknown as Map; + await conversation.save(); + + return conversation; + } + + async clearConversationUnreadForUser(conversationId: string, userId: string): Promise { + const conversation = await this.conversationModel.findById(conversationId).exec(); + if (!conversation) { + return; + } + + const unreadMap = new Map( + Object.entries((conversation.unreadCountByUser as unknown as Record) ?? {}), + ); + unreadMap.set(userId, 0); + conversation.unreadCountByUser = unreadMap as unknown as Map; + await conversation.save(); + } + + async unsendMessage(messageId: string, senderId: string): Promise { + return this.messageModel + .findOneAndUpdate( + { _id: new Types.ObjectId(messageId), senderId: new Types.ObjectId(senderId) }, + { + isUnsent: true, + content: '', + mediaUrl: '', + messageType: 'text', + }, + { new: true }, + ) + .exec(); + } + + async findManyMessages(filter: FilterQuery): Promise { + return this.messageModel.find(filter).exec(); + } + + async createBlock(blockerId: string, blockedId: string): Promise { + await this.chatBlockModel + .updateOne( + { blockerId: new Types.ObjectId(blockerId), blockedId: new Types.ObjectId(blockedId) }, + { + $setOnInsert: { + blockerId: new Types.ObjectId(blockerId), + blockedId: new Types.ObjectId(blockedId), + }, + }, + { upsert: true }, + ) + .exec(); + } + + async removeBlock(blockerId: string, blockedId: string): Promise { + await this.chatBlockModel + .deleteOne({ blockerId: new Types.ObjectId(blockerId), blockedId: new Types.ObjectId(blockedId) }) + .exec(); + } + + async findBlock(blockerId: string, blockedId: string): Promise { + return this.chatBlockModel + .findOne({ blockerId: new Types.ObjectId(blockerId), blockedId: new Types.ObjectId(blockedId) }) + .exec(); + } + + async findAnyBlockBetween(userAId: string, userBId: string): Promise { + return this.chatBlockModel + .findOne({ + $or: [ + { blockerId: new Types.ObjectId(userAId), blockedId: new Types.ObjectId(userBId) }, + { blockerId: new Types.ObjectId(userBId), blockedId: new Types.ObjectId(userAId) }, + ], + }) + .exec(); + } + + async findBlocksByBlocker(blockerId: string): Promise { + return this.chatBlockModel + .find({ blockerId: new Types.ObjectId(blockerId) }) + .populate({ path: 'blockedId', select: 'name username stageName avatar isVerified isDisabled' }) + .sort({ createdAt: -1 }) + .exec(); + } +} diff --git a/src/modules/chat/chat.service.ts b/src/modules/chat/chat.service.ts new file mode 100644 index 0000000..c2b2d93 --- /dev/null +++ b/src/modules/chat/chat.service.ts @@ -0,0 +1,250 @@ +import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common'; +import { Types } from 'mongoose'; +import { decodeOffsetCursor, encodeOffsetCursor } from '../../common/utils/cursor.util'; +import { UsersRepository } from '../users/users.repository'; +import { CreateConversationDto } from './dto/create-conversation.dto'; +import { MessageQueryDto } from './dto/message-query.dto'; +import { SendMessageDto } from './dto/send-message.dto'; +import { ChatRepository } from './chat.repository'; + +@Injectable() +export class ChatService { + constructor( + private readonly chatRepository: ChatRepository, + private readonly usersRepository: UsersRepository, + ) {} + + async createConversation(currentUserId: string, dto: CreateConversationDto) { + const uniqueParticipantIds = Array.from(new Set([currentUserId, ...dto.participantIds])); + if (uniqueParticipantIds.length < 2) { + throw new BadRequestException('Conversation must include at least 2 participants'); + } + + for (const participantId of uniqueParticipantIds) { + if (!Types.ObjectId.isValid(participantId)) { + throw new BadRequestException('Invalid participant id'); + } + } + + const users = await Promise.all(uniqueParticipantIds.map((id) => this.usersRepository.findById(id))); + if (users.some((u) => !u || u.isDisabled)) { + throw new BadRequestException('One or more participants are invalid or disabled'); + } + + const isGroup = dto.isGroup ?? uniqueParticipantIds.length > 2; + if (!isGroup && uniqueParticipantIds.length !== 2) { + throw new BadRequestException('Direct conversation must contain exactly 2 participants'); + } + + if (!isGroup) { + const otherId = uniqueParticipantIds.find((id) => id !== currentUserId) as string; + const block = await this.chatRepository.findAnyBlockBetween(currentUserId, otherId); + if (block) { + throw new ForbiddenException('You cannot start chat with this user'); + } + const existing = await this.chatRepository.findDirectConversation(currentUserId, otherId); + if (existing) { + return existing; + } + } + + return this.chatRepository.createConversation({ + participantIds: uniqueParticipantIds, + isGroup, + title: dto.title, + createdBy: currentUserId, + }); + } + + async getMyConversations(currentUserId: string, query: MessageQueryDto) { + const page = query.page ?? 1; + const limit = query.limit ?? 20; + const cursorOffset = decodeOffsetCursor(query.cursor); + const skip = cursorOffset ?? (page - 1) * limit; + + const [items, total] = await Promise.all([ + this.chatRepository.findConversationsForUser(currentUserId, skip, limit), + this.chatRepository.countConversationsForUser(currentUserId), + ]); + + const mappedItems = items.map((conversation) => { + const unreadMap = (conversation.unreadCountByUser as unknown as Record) ?? {}; + return { + ...conversation.toObject(), + unreadCount: unreadMap[currentUserId] ?? 0, + lastMessageAt: conversation.lastMessageAt ?? null, + }; + }); + const nextOffset = skip + mappedItems.length; + const nextCursor = nextOffset < total ? encodeOffsetCursor(nextOffset) : null; + + return { + items: mappedItems, + page, + limit, + total, + totalPages: Math.ceil(total / limit) || 1, + nextCursor, + }; + } + + async getMessages(currentUserId: string, conversationId: string, query: MessageQueryDto) { + const conversation = await this.assertConversationMember(currentUserId, conversationId); + const page = query.page ?? 1; + const limit = query.limit ?? 20; + const cursorOffset = decodeOffsetCursor(query.cursor); + const skip = cursorOffset ?? (page - 1) * limit; + + const [items, total] = await Promise.all([ + this.chatRepository.findMessages(conversation.id, skip, limit), + this.chatRepository.countMessages(conversation.id), + ]); + + await this.chatRepository.clearConversationUnreadForUser(conversation.id, currentUserId); + const nextOffset = skip + items.length; + const nextCursor = nextOffset < total ? encodeOffsetCursor(nextOffset) : null; + + return { + items, + page, + limit, + total, + totalPages: Math.ceil(total / limit) || 1, + nextCursor, + }; + } + + async sendMessage(currentUserId: string, dto: SendMessageDto) { + const conversation = await this.assertConversationMember(currentUserId, dto.conversationId); + await this.assertNoChatBlockInConversation(currentUserId, conversation.participantIds.map((id) => id.toString())); + const messageType = dto.messageType ?? 'text'; + const content = dto.content?.trim() ?? ''; + const mediaUrl = dto.mediaUrl?.trim() ?? ''; + + if (messageType === 'text' && !content) { + throw new BadRequestException('Text message content is required'); + } + + if (messageType !== 'text' && !mediaUrl) { + throw new BadRequestException('mediaUrl is required for non-text messages'); + } + + const message = await this.chatRepository.createMessage({ + conversationId: conversation.id, + senderId: currentUserId, + content, + messageType, + mediaUrl, + }); + + const preview = messageType === 'text' ? content : `${messageType} message`; + await this.chatRepository.updateConversationAfterNewMessage( + conversation.id, + message.id, + currentUserId, + preview, + ); + + return message; + } + + async markMessageSeen(currentUserId: string, messageId: string) { + const message = await this.chatRepository.findMessageById(messageId); + if (!message) { + throw new NotFoundException('Message not found'); + } + + await this.assertConversationMember(currentUserId, message.conversationId.toString()); + await this.chatRepository.markMessageSeen(message.id, currentUserId); + await this.chatRepository.clearConversationUnreadForUser(message.conversationId.toString(), currentUserId); + + return { success: true }; + } + + async unsendMessage(currentUserId: string, messageId: string) { + const message = await this.chatRepository.findMessageById(messageId); + if (!message) { + throw new NotFoundException('Message not found'); + } + if (message.senderId.toString() !== currentUserId) { + throw new ForbiddenException('You can only unsend your own messages'); + } + + const updated = await this.chatRepository.unsendMessage(messageId, currentUserId); + if (!updated) { + throw new NotFoundException('Message not found'); + } + return updated; + } + + async blockUser(currentUserId: string, targetUserId: string) { + if (!Types.ObjectId.isValid(targetUserId)) { + throw new BadRequestException('Invalid target user id'); + } + if (currentUserId === targetUserId) { + throw new BadRequestException('You cannot block yourself'); + } + + const target = await this.usersRepository.findById(targetUserId); + if (!target) { + throw new NotFoundException('Target user not found'); + } + + await this.chatRepository.createBlock(currentUserId, targetUserId); + return { blocked: true, targetUserId }; + } + + async unblockUser(currentUserId: string, targetUserId: string) { + if (!Types.ObjectId.isValid(targetUserId)) { + throw new BadRequestException('Invalid target user id'); + } + await this.chatRepository.removeBlock(currentUserId, targetUserId); + return { blocked: false, targetUserId }; + } + + async getBlockStatus(currentUserId: string, targetUserId: string) { + if (!Types.ObjectId.isValid(targetUserId)) { + throw new BadRequestException('Invalid target user id'); + } + + const iBlocked = !!(await this.chatRepository.findBlock(currentUserId, targetUserId)); + const blockedMe = !!(await this.chatRepository.findBlock(targetUserId, currentUserId)); + + return { targetUserId, iBlocked, blockedMe }; + } + + async getMyBlockedUsers(currentUserId: string) { + const items = await this.chatRepository.findBlocksByBlocker(currentUserId); + return { items }; + } + + async assertConversationMember(userId: string, conversationId: string) { + if (!Types.ObjectId.isValid(conversationId)) { + throw new BadRequestException('Invalid conversation id'); + } + + const conversation = await this.chatRepository.findConversationById(conversationId); + if (!conversation) { + throw new NotFoundException('Conversation not found'); + } + + const isMember = conversation.participantIds.some((id) => id.toString() === userId); + if (!isMember) { + throw new ForbiddenException('You are not a member of this conversation'); + } + + return conversation; + } + + private async assertNoChatBlockInConversation(currentUserId: string, participantIds: string[]) { + for (const participantId of participantIds) { + if (participantId === currentUserId) { + continue; + } + const block = await this.chatRepository.findAnyBlockBetween(currentUserId, participantId); + if (block) { + throw new ForbiddenException('Cannot send message because one of participants is blocked'); + } + } + } +} diff --git a/src/modules/chat/dto/create-conversation.dto.ts b/src/modules/chat/dto/create-conversation.dto.ts new file mode 100644 index 0000000..46add12 --- /dev/null +++ b/src/modules/chat/dto/create-conversation.dto.ts @@ -0,0 +1,19 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsArray, IsBoolean, IsOptional, IsString, Length } from 'class-validator'; + +export class CreateConversationDto { + @IsArray() + @IsString({ each: true }) + participantIds!: string[]; + + @ApiPropertyOptional({ default: false }) + @IsOptional() + @IsBoolean() + isGroup?: boolean; + + @ApiPropertyOptional({ maxLength: 120 }) + @IsOptional() + @IsString() + @Length(1, 120) + title?: string; +} diff --git a/src/modules/chat/dto/message-query.dto.ts b/src/modules/chat/dto/message-query.dto.ts new file mode 100644 index 0000000..b3e3527 --- /dev/null +++ b/src/modules/chat/dto/message-query.dto.ts @@ -0,0 +1,3 @@ +import { PaginationQueryDto } from '../../../common/dto/pagination-query.dto'; + +export class MessageQueryDto extends PaginationQueryDto {} diff --git a/src/modules/chat/dto/send-message.dto.ts b/src/modules/chat/dto/send-message.dto.ts new file mode 100644 index 0000000..c330c17 --- /dev/null +++ b/src/modules/chat/dto/send-message.dto.ts @@ -0,0 +1,23 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsEnum, IsOptional, IsString, IsUrl, Length } from 'class-validator'; + +export class SendMessageDto { + @IsString() + conversationId!: string; + + @ApiPropertyOptional({ maxLength: 4000 }) + @IsOptional() + @IsString() + @Length(1, 4000) + content?: string; + + @ApiPropertyOptional({ enum: ['text', 'image', 'video', 'audio'], default: 'text' }) + @IsOptional() + @IsEnum(['text', 'image', 'video', 'audio']) + messageType?: 'text' | 'image' | 'video' | 'audio'; + + @ApiPropertyOptional() + @IsOptional() + @IsUrl({ require_tld: false }) + mediaUrl?: string; +} diff --git a/src/modules/chat/schemas/chat-block.schema.ts b/src/modules/chat/schemas/chat-block.schema.ts new file mode 100644 index 0000000..feaccc3 --- /dev/null +++ b/src/modules/chat/schemas/chat-block.schema.ts @@ -0,0 +1,17 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { HydratedDocument, Types } from 'mongoose'; +import { User } from '../../users/schemas/user.schema'; + +export type ChatBlockDocument = HydratedDocument; + +@Schema({ timestamps: true, versionKey: false }) +export class ChatBlock { + @Prop({ type: Types.ObjectId, ref: User.name, required: true, index: true }) + blockerId!: Types.ObjectId; + + @Prop({ type: Types.ObjectId, ref: User.name, required: true, index: true }) + blockedId!: Types.ObjectId; +} + +export const ChatBlockSchema = SchemaFactory.createForClass(ChatBlock); +ChatBlockSchema.index({ blockerId: 1, blockedId: 1 }, { unique: true }); diff --git a/src/modules/chat/schemas/conversation.schema.ts b/src/modules/chat/schemas/conversation.schema.ts new file mode 100644 index 0000000..e618946 --- /dev/null +++ b/src/modules/chat/schemas/conversation.schema.ts @@ -0,0 +1,37 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { HydratedDocument, Types } from 'mongoose'; +import { User } from '../../users/schemas/user.schema'; + +export type ConversationDocument = HydratedDocument; + +@Schema({ timestamps: true, versionKey: false }) +export class Conversation { + @Prop({ type: [Types.ObjectId], ref: User.name, required: true, index: true }) + participantIds!: Types.ObjectId[]; + + @Prop({ default: false, index: true }) + isGroup!: boolean; + + @Prop({ default: '', maxlength: 120, trim: true }) + title!: string; + + @Prop({ type: Types.ObjectId, ref: User.name, required: false, index: true }) + createdBy?: Types.ObjectId; + + @Prop({ type: Types.ObjectId, required: false, index: true }) + lastMessageId?: Types.ObjectId; + + @Prop({ default: '', maxlength: 4000 }) + lastMessageText!: string; + + @Prop({ type: Date, required: false, index: true }) + lastMessageAt?: Date; + + @Prop({ type: Map, of: Number, default: {} }) + unreadCountByUser!: Map; +} + +export const ConversationSchema = SchemaFactory.createForClass(Conversation); +ConversationSchema.index({ participantIds: 1, updatedAt: -1 }); +ConversationSchema.index({ lastMessageAt: -1, updatedAt: -1 }); +ConversationSchema.index({ participantIds: 1, isGroup: 1, lastMessageAt: -1 }); diff --git a/src/modules/chat/schemas/message.schema.ts b/src/modules/chat/schemas/message.schema.ts new file mode 100644 index 0000000..9ff6b2f --- /dev/null +++ b/src/modules/chat/schemas/message.schema.ts @@ -0,0 +1,33 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { HydratedDocument, Types } from 'mongoose'; +import { User } from '../../users/schemas/user.schema'; + +export type MessageDocument = HydratedDocument; + +@Schema({ timestamps: true, versionKey: false }) +export class Message { + @Prop({ type: Types.ObjectId, required: true, index: true }) + conversationId!: Types.ObjectId; + + @Prop({ type: Types.ObjectId, ref: User.name, required: true, index: true }) + senderId!: Types.ObjectId; + + @Prop({ required: false, default: '', maxlength: 4000 }) + content!: string; + + @Prop({ enum: ['text', 'image', 'video', 'audio'], default: 'text', index: true }) + messageType!: 'text' | 'image' | 'video' | 'audio'; + + @Prop({ required: false, default: '' }) + mediaUrl!: string; + + @Prop({ type: [Types.ObjectId], ref: User.name, default: [] }) + seenBy!: Types.ObjectId[]; + + @Prop({ default: false, index: true }) + isUnsent!: boolean; +} + +export const MessageSchema = SchemaFactory.createForClass(Message); +MessageSchema.index({ conversationId: 1, createdAt: -1 }); +MessageSchema.index({ conversationId: 1, isUnsent: 1, createdAt: -1 }); diff --git a/src/modules/comments/comments.controller.ts b/src/modules/comments/comments.controller.ts new file mode 100644 index 0000000..3495321 --- /dev/null +++ b/src/modules/comments/comments.controller.ts @@ -0,0 +1,50 @@ +import { Controller, Delete, Get, Param, Post, Query, Body, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { CurrentUser } from '../../common/decorators/current-user.decorator'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { SuperAdminJwtAuthGuard } from '../../common/guards/super-admin-jwt-auth.guard'; +import { JwtPayload } from '../../common/interfaces/jwt-payload.interface'; +import { CommentQueryDto } from './dto/comment-query.dto'; +import { CreateCommentDto } from './dto/create-comment.dto'; +import { CommentsService } from './comments.service'; + +@ApiTags('Comments') +@Controller('comments') +export class CommentsController { + constructor(private readonly commentsService: CommentsService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post() + async create(@CurrentUser() user: JwtPayload, @Body() dto: CreateCommentDto) { + return this.commentsService.create(user.sub, dto); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('post/:postId') + async findByPost(@Param('postId') postId: string, @Query() query: CommentQueryDto) { + return this.commentsService.findByPost(postId, query); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':commentId/replies') + async findReplies(@Param('commentId') commentId: string, @Query() query: CommentQueryDto) { + return this.commentsService.findReplies(commentId, query); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Delete(':commentId') + async remove(@CurrentUser() user: JwtPayload, @Param('commentId') commentId: string) { + return this.commentsService.remove(user.sub, commentId); + } + + @ApiBearerAuth() + @UseGuards(SuperAdminJwtAuthGuard) + @Delete('admin/:commentId') + async adminRemove(@CurrentUser() user: JwtPayload, @Param('commentId') commentId: string) { + return this.commentsService.removeBySuperAdmin(user.email ?? user.sub, commentId); + } +} diff --git a/src/modules/comments/comments.module.ts b/src/modules/comments/comments.module.ts new file mode 100644 index 0000000..966d07d --- /dev/null +++ b/src/modules/comments/comments.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { AuditModule } from '../audit/audit.module'; +import { PostsModule } from '../posts/posts.module'; +import { Comment, CommentSchema } from './schemas/comment.schema'; +import { CommentsController } from './comments.controller'; +import { CommentsService } from './comments.service'; +import { CommentsRepository } from './comments.repository'; + +@Module({ + imports: [ + AuditModule, + MongooseModule.forFeature([{ name: Comment.name, schema: CommentSchema }]), + PostsModule, + ], + controllers: [CommentsController], + providers: [CommentsService, CommentsRepository], + exports: [CommentsService, CommentsRepository], +}) +export class CommentsModule {} diff --git a/src/modules/comments/comments.repository.ts b/src/modules/comments/comments.repository.ts new file mode 100644 index 0000000..a6431b0 --- /dev/null +++ b/src/modules/comments/comments.repository.ts @@ -0,0 +1,77 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { ClientSession, FilterQuery, Model, Types } from 'mongoose'; +import { Comment, CommentDocument } from './schemas/comment.schema'; + +@Injectable() +export class CommentsRepository { + constructor(@InjectModel(Comment.name) private readonly commentModel: Model) {} + + private withActiveFilter>(filter: T): FilterQuery { + return { + ...filter, + isDeleted: { $ne: true }, + }; + } + + async create( + payload: { postId: string; authorId: string; content: string; parentCommentId?: string }, + session?: ClientSession, + ) { + return this.commentModel.create({ + postId: new Types.ObjectId(payload.postId), + authorId: new Types.ObjectId(payload.authorId), + content: payload.content, + ...(payload.parentCommentId ? { parentCommentId: new Types.ObjectId(payload.parentCommentId) } : {}), + }, { session }); + } + + async findById(commentId: string): Promise { + if (!Types.ObjectId.isValid(commentId)) { + return null; + } + + return this.commentModel + .findOne({ _id: new Types.ObjectId(commentId), isDeleted: { $ne: true } }) + .exec(); + } + + async deleteById(commentId: string, deletedBy?: string, session?: ClientSession): Promise { + if (!Types.ObjectId.isValid(commentId)) { + return false; + } + + const deletedByObjectId = + deletedBy && Types.ObjectId.isValid(deletedBy) ? new Types.ObjectId(deletedBy) : null; + + const updated = await this.commentModel + .findOneAndUpdate( + { _id: new Types.ObjectId(commentId), isDeleted: { $ne: true } }, + { isDeleted: true, deletedAt: new Date(), deletedBy: deletedByObjectId }, + { new: false, session }, + ) + .exec(); + + return !!updated; + } + + async findMany(filter: FilterQuery, skip: number, limit: number) { + return this.commentModel + .find(this.withActiveFilter(filter)) + .populate({ path: 'authorId', select: 'name username avatar stageName isVerified' }) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit) + .exec(); + } + + async count(filter: FilterQuery): Promise { + return this.commentModel.countDocuments(this.withActiveFilter(filter)).exec(); + } + + async countByPost(postId: string): Promise { + return this.commentModel + .countDocuments({ postId: new Types.ObjectId(postId), isDeleted: { $ne: true } }) + .exec(); + } +} diff --git a/src/modules/comments/comments.service.ts b/src/modules/comments/comments.service.ts new file mode 100644 index 0000000..4d39237 --- /dev/null +++ b/src/modules/comments/comments.service.ts @@ -0,0 +1,114 @@ +import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common'; +import { AuditService } from '../audit/audit.service'; +import { PostsRepository } from '../posts/posts.repository'; +import { CommentQueryDto } from './dto/comment-query.dto'; +import { CreateCommentDto } from './dto/create-comment.dto'; +import { CommentsRepository } from './comments.repository'; + +@Injectable() +export class CommentsService { + constructor( + private readonly commentsRepository: CommentsRepository, + private readonly postsRepository: PostsRepository, + private readonly auditService: AuditService, + ) {} + + async create(userId: string, dto: CreateCommentDto) { + const post = await this.postsRepository.findById(dto.postId); + if (!post) { + throw new NotFoundException('Post not found'); + } + + if (dto.parentCommentId) { + const parent = await this.commentsRepository.findById(dto.parentCommentId); + if (!parent || parent.postId.toString() !== dto.postId) { + throw new NotFoundException('Parent comment not found'); + } + } + + const comment = await this.commentsRepository.create({ + postId: dto.postId, + authorId: userId, + content: dto.content, + parentCommentId: dto.parentCommentId, + }); + await this.syncCommentsCount(dto.postId); + return comment; + } + + async remove(userId: string, commentId: string) { + const comment = await this.commentsRepository.findById(commentId); + if (!comment) { + throw new NotFoundException('Comment not found'); + } + + if (comment.authorId.toString() !== userId) { + throw new ForbiddenException('You can only delete your own comments'); + } + + await this.commentsRepository.deleteById(commentId, userId); + await this.syncCommentsCount(comment.postId.toString()); + return { success: true }; + } + + async removeBySuperAdmin(superAdminIdentifier: string, commentId: string) { + const comment = await this.commentsRepository.findById(commentId); + if (!comment) { + throw new NotFoundException('Comment not found'); + } + + await this.commentsRepository.deleteById(commentId, superAdminIdentifier); + await this.syncCommentsCount(comment.postId.toString()); + await this.auditService.logSuperAdminAction( + superAdminIdentifier, + 'comment_delete', + 'comment', + commentId, + { postId: comment.postId.toString() }, + ); + return { success: true, message: 'Comment deleted by superadmin' }; + } + + async findByPost(postId: string, query: CommentQueryDto) { + const page = query.page ?? 1; + const limit = query.limit ?? 20; + const skip = (page - 1) * limit; + + const [items, total] = await Promise.all([ + this.commentsRepository.findMany({ postId, parentCommentId: { $exists: false } }, skip, limit), + this.commentsRepository.count({ postId, parentCommentId: { $exists: false } }), + ]); + + return { + items, + page, + limit, + total, + totalPages: Math.ceil(total / limit) || 1, + }; + } + + async findReplies(parentCommentId: string, query: CommentQueryDto) { + const page = query.page ?? 1; + const limit = query.limit ?? 20; + const skip = (page - 1) * limit; + + const [items, total] = await Promise.all([ + this.commentsRepository.findMany({ parentCommentId }, skip, limit), + this.commentsRepository.count({ parentCommentId }), + ]); + + return { + items, + page, + limit, + total, + totalPages: Math.ceil(total / limit) || 1, + }; + } + + private async syncCommentsCount(postId: string): Promise { + const totalComments = await this.commentsRepository.countByPost(postId); + await this.postsRepository.setCommentsCount(postId, totalComments); + } +} diff --git a/src/modules/comments/dto/comment-query.dto.ts b/src/modules/comments/dto/comment-query.dto.ts new file mode 100644 index 0000000..62e7309 --- /dev/null +++ b/src/modules/comments/dto/comment-query.dto.ts @@ -0,0 +1,3 @@ +import { PaginationQueryDto } from '../../../common/dto/pagination-query.dto'; + +export class CommentQueryDto extends PaginationQueryDto {} diff --git a/src/modules/comments/dto/create-comment.dto.ts b/src/modules/comments/dto/create-comment.dto.ts new file mode 100644 index 0000000..8a73741 --- /dev/null +++ b/src/modules/comments/dto/create-comment.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsMongoId, IsOptional, IsString, Length } from 'class-validator'; + +export class CreateCommentDto { + @ApiProperty() + @IsMongoId() + postId!: string; + + @ApiProperty() + @IsString() + @Length(1, 1000) + content!: string; + + @ApiPropertyOptional() + @IsOptional() + @IsMongoId() + parentCommentId?: string; +} diff --git a/src/modules/comments/dto/update-comment.dto.ts b/src/modules/comments/dto/update-comment.dto.ts new file mode 100644 index 0000000..39a1aa1 --- /dev/null +++ b/src/modules/comments/dto/update-comment.dto.ts @@ -0,0 +1,8 @@ +import { IsOptional, IsString, Length } from 'class-validator'; + +export class UpdateCommentDto { + @IsOptional() + @IsString() + @Length(1, 1000) + content?: string; +} diff --git a/src/modules/comments/schemas/comment.schema.ts b/src/modules/comments/schemas/comment.schema.ts new file mode 100644 index 0000000..deb4726 --- /dev/null +++ b/src/modules/comments/schemas/comment.schema.ts @@ -0,0 +1,34 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { HydratedDocument, Types } from 'mongoose'; +import { Post } from '../../posts/schemas/post.schema'; +import { User } from '../../users/schemas/user.schema'; + +export type CommentDocument = HydratedDocument; + +@Schema({ timestamps: true, versionKey: false }) +export class Comment { + @Prop({ type: Types.ObjectId, ref: Post.name, required: true, index: true }) + postId!: Types.ObjectId; + + @Prop({ type: Types.ObjectId, ref: User.name, required: true, index: true }) + authorId!: Types.ObjectId; + + @Prop({ type: Types.ObjectId, required: false, index: true }) + parentCommentId?: Types.ObjectId; + + @Prop({ required: true, maxlength: 1000 }) + content!: string; + + @Prop({ default: false, index: true }) + isDeleted!: boolean; + + @Prop({ type: Date, default: null }) + deletedAt?: Date | null; + + @Prop({ type: Types.ObjectId, ref: User.name, default: null }) + deletedBy?: Types.ObjectId | null; +} + +export const CommentSchema = SchemaFactory.createForClass(Comment); +CommentSchema.index({ postId: 1, createdAt: -1 }); +CommentSchema.index({ postId: 1, parentCommentId: 1, isDeleted: 1, createdAt: -1 }); diff --git a/src/modules/email/email.module.ts b/src/modules/email/email.module.ts new file mode 100644 index 0000000..fff2146 --- /dev/null +++ b/src/modules/email/email.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { EmailService } from './email.service'; + +@Module({ + providers: [EmailService], + exports: [EmailService], +}) +export class EmailModule {} diff --git a/src/modules/email/email.service.ts b/src/modules/email/email.service.ts new file mode 100644 index 0000000..7bcc910 --- /dev/null +++ b/src/modules/email/email.service.ts @@ -0,0 +1,134 @@ +import { Injectable, Logger, ServiceUnavailableException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import * as nodemailer from 'nodemailer'; +import SMTPTransport from 'nodemailer/lib/smtp-transport'; + +@Injectable() +export class EmailService { + private readonly logger = new Logger(EmailService.name); + private transporter: nodemailer.Transporter | null = null; + + constructor(private readonly configService: ConfigService) {} + + async sendVerificationCode(email: string, code: string, expiresMinutes: number): Promise { + const subject = 'طھط£ظƒظٹط¯ ط§ظ„ط¨ط±ظٹط¯ ط§ظ„ط¥ظ„ظƒطھط±ظˆظ†ظٹ - Oudelaa'; + const text = [ + 'ظ…ط±ط­ط¨ط§ظ‹طŒ', + `ط±ظ…ط² طھط£ظƒظٹط¯ ط§ظ„ط­ط³ط§ط¨ ط§ظ„ط®ط§طµ ط¨ظƒ ظ‡ظˆ: ${code}`, + `طµظ„ط§ط­ظٹط© ط§ظ„ط±ظ…ط²: ${expiresMinutes} ط¯ظ‚ظٹظ‚ط©.`, + 'ط¥ط°ط§ ظ„ظ… طھط·ظ„ط¨ ظ‡ط°ط§ ط§ظ„ط±ظ…ط²طŒ طھط¬ط§ظ‡ظ„ ظ‡ط°ظ‡ ط§ظ„ط±ط³ط§ظ„ط©.', + ].join('\n'); + const html = this.buildCodeEmailHtml({ + title: 'طھط£ظƒظٹط¯ ط§ظ„ط¨ط±ظٹط¯ ط§ظ„ط¥ظ„ظƒطھط±ظˆظ†ظٹ', + intro: 'ط§ط³طھط®ط¯ظ… ط§ظ„ط±ظ…ط² ط§ظ„طھط§ظ„ظٹ ظ„ط¥ظƒظ…ط§ظ„ طھظپط¹ظٹظ„ ط­ط³ط§ط¨ظƒ ظپظٹ Oudelaa:', + code, + expiresMinutes, + footerNote: 'ط¥ط°ط§ ظ„ظ… طھط·ظ„ط¨ ظ‡ط°ط§ ط§ظ„ط±ظ…ط²طŒ ظٹظ…ظƒظ†ظƒ طھط¬ط§ظ‡ظ„ ط§ظ„ط±ط³ط§ظ„ط© ط¨ط£ظ…ط§ظ†.', + }); + await this.send(email, subject, text, html); + } + + async sendPasswordResetCode(email: string, code: string, expiresMinutes: number): Promise { + const subject = 'ط¥ط¹ط§ط¯ط© طھط¹ظٹظٹظ† ظƒظ„ظ…ط© ط§ظ„ظ…ط±ظˆط± - Oudelaa'; + const text = [ + 'ظ…ط±ط­ط¨ط§ظ‹طŒ', + `ط±ظ…ط² ط¥ط¹ط§ط¯ط© طھط¹ظٹظٹظ† ظƒظ„ظ…ط© ط§ظ„ظ…ط±ظˆط± ظ‡ظˆ: ${code}`, + `طµظ„ط§ط­ظٹط© ط§ظ„ط±ظ…ط²: ${expiresMinutes} ط¯ظ‚ظٹظ‚ط©.`, + 'ط¥ط°ط§ ظ„ظ… طھط·ظ„ط¨ ط¥ط¹ط§ط¯ط© طھط¹ظٹظٹظ† ظƒظ„ظ…ط© ط§ظ„ظ…ط±ظˆط±طŒ ظ†ظ†طµط­ظƒ ط¨طھط؛ظٹظٹط± ظƒظ„ظ…ط© ط§ظ„ظ…ط±ظˆط± ظ…ط¨ط§ط´ط±ط©.', + ].join('\n'); + const html = this.buildCodeEmailHtml({ + title: 'ط¥ط¹ط§ط¯ط© طھط¹ظٹظٹظ† ظƒظ„ظ…ط© ط§ظ„ظ…ط±ظˆط±', + intro: 'ط§ط³طھط®ط¯ظ… ط§ظ„ط±ظ…ط² ط§ظ„طھط§ظ„ظٹ ظ„ط¥ط¹ط§ط¯ط© طھط¹ظٹظٹظ† ظƒظ„ظ…ط© ط§ظ„ظ…ط±ظˆط±:', + code, + expiresMinutes, + footerNote: 'ط¥ط°ط§ ظ„ظ… طھط·ظ„ط¨ ط¥ط¹ط§ط¯ط© ط§ظ„طھط¹ظٹظٹظ†طŒ طھط¬ط§ظ‡ظ„ ظ‡ط°ظ‡ ط§ظ„ط±ط³ط§ظ„ط© ظˆظ‚ظ… ط¨ظ…ط±ط§ط¬ط¹ط© ط£ظ…ط§ظ† ط­ط³ط§ط¨ظƒ.', + }); + await this.send(email, subject, text, html); + } + + private buildCodeEmailHtml(params: { + title: string; + intro: string; + code: string; + expiresMinutes: number; + footerNote: string; + }): string { + return ` +
+
+

${params.title}

+

${params.intro}

+
+ ${params.code} +
+

طµظ„ط§ط­ظٹط© ط§ظ„ط±ظ…ط²: ${params.expiresMinutes} ط¯ظ‚ظٹظ‚ط©

+

${params.footerNote}

+
+

Oudelaa Team

+
+
+ `; + } + + private async send(to: string, subject: string, text: string, html: string): Promise { + const enabled = this.configService.get('email.enabled', { infer: true }); + if (!enabled) { + return; + } + + const fromName = this.configService.get('email.fromName', { infer: true }) ?? 'Oudelaa'; + const fromEmail = this.configService.get('email.fromEmail', { infer: true }) ?? ''; + if (!fromEmail) { + throw new ServiceUnavailableException('Email sender is not configured'); + } + + const transporter = this.getTransporter(); + try { + await transporter.sendMail({ + from: `${fromName} <${fromEmail}>`, + to, + subject, + text, + html, + }); + } catch (error) { + this.logger.error(`Failed to send email to ${to}`, error as Error); + const nodeEnv = this.configService.get('nodeEnv', { infer: true }); + if (nodeEnv === 'development') { + const err = error as Error & { code?: string; message?: string }; + throw new ServiceUnavailableException( + `Failed to send verification email (${err.code ?? 'SMTP_ERROR'}: ${err.message ?? 'unknown'})`, + ); + } + throw new ServiceUnavailableException('Failed to send verification email'); + } + } + + private getTransporter(): nodemailer.Transporter { + if (this.transporter) { + return this.transporter; + } + + const host = this.configService.get('email.smtpHost', { infer: true }) ?? ''; + const port = this.configService.get('email.smtpPort', { infer: true }) ?? 587; + const secure = this.configService.get('email.smtpSecure', { infer: true }) ?? false; + const user = this.configService.get('email.smtpUser', { infer: true }) ?? ''; + const pass = this.configService.get('email.smtpPass', { infer: true }) ?? ''; + + if (!host || !user || !pass) { + throw new ServiceUnavailableException('SMTP settings are not configured'); + } + + this.transporter = nodemailer.createTransport({ + host, + port, + secure, + auth: { + user, + pass, + }, + }); + + return this.transporter; + } +} diff --git a/src/modules/feed/dto/feed-query.dto.ts b/src/modules/feed/dto/feed-query.dto.ts new file mode 100644 index 0000000..12e3c3f --- /dev/null +++ b/src/modules/feed/dto/feed-query.dto.ts @@ -0,0 +1,22 @@ +import { PaginationQueryDto } from '../../../common/dto/pagination-query.dto'; +import { IsBoolean, IsEnum, IsNumber, IsOptional, Max, Min } from 'class-validator'; +import { Type } from 'class-transformer'; +import { PostType } from '../../../common/enums/post-type.enum'; + +export class FeedQueryDto extends PaginationQueryDto { + @IsOptional() + @IsEnum(PostType) + preferredPostType?: PostType; + + @IsOptional() + @Type(() => Boolean) + @IsBoolean() + followingOnly?: boolean; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(1) + @Max(500) + radiusKm?: number; +} diff --git a/src/modules/feed/feed.controller.ts b/src/modules/feed/feed.controller.ts new file mode 100644 index 0000000..31b2933 --- /dev/null +++ b/src/modules/feed/feed.controller.ts @@ -0,0 +1,27 @@ +import { Controller, Get, Query, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { CurrentUser } from '../../common/decorators/current-user.decorator'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { JwtPayload } from '../../common/interfaces/jwt-payload.interface'; +import { FeedQueryDto } from './dto/feed-query.dto'; +import { FeedService } from './feed.service'; + +@ApiTags('Feed') +@Controller('feed') +export class FeedController { + constructor(private readonly feedService: FeedService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('me') + async myFeed(@CurrentUser() user: JwtPayload, @Query() query: FeedQueryDto) { + return this.feedService.getMyFeed(user.sub, query); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('trending') + async trending(@Query() query: FeedQueryDto) { + return this.feedService.getTrending(query); + } +} diff --git a/src/modules/feed/feed.module.ts b/src/modules/feed/feed.module.ts new file mode 100644 index 0000000..7ee5e7a --- /dev/null +++ b/src/modules/feed/feed.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { Follow, FollowSchema } from '../follows/schemas/follow.schema'; +import { Post, PostSchema } from '../posts/schemas/post.schema'; +import { UsersModule } from '../users/users.module'; +import { FeedController } from './feed.controller'; +import { FeedService } from './feed.service'; +import { FeedRepository } from './feed.repository'; + +@Module({ + imports: [ + UsersModule, + MongooseModule.forFeature([ + { name: Post.name, schema: PostSchema }, + { name: Follow.name, schema: FollowSchema }, + ]), + ], + controllers: [FeedController], + providers: [FeedService, FeedRepository], + exports: [FeedService], +}) +export class FeedModule {} diff --git a/src/modules/feed/feed.repository.ts b/src/modules/feed/feed.repository.ts new file mode 100644 index 0000000..0a662ac --- /dev/null +++ b/src/modules/feed/feed.repository.ts @@ -0,0 +1,58 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { FilterQuery, Model, Types } from 'mongoose'; +import { Follow, FollowDocument } from '../follows/schemas/follow.schema'; +import { Post, PostDocument } from '../posts/schemas/post.schema'; + +@Injectable() +export class FeedRepository { + constructor( + @InjectModel(Post.name) private readonly postModel: Model, + @InjectModel(Follow.name) private readonly followModel: Model, + ) {} + + async findFollowingIds(userId: string): Promise { + const rows = await this.followModel + .find({ followerId: new Types.ObjectId(userId) }) + .select({ followingId: 1 }) + .lean() + .exec(); + + return rows.map((row) => row.followingId.toString()); + } + + async findCandidatePosts( + filter: FilterQuery, + limit: number, + ): Promise { + const activeFilter: FilterQuery = { + ...filter, + isDeleted: { $ne: true }, + }; + + return this.postModel + .find(activeFilter) + .populate({ + path: 'authorId', + select: + 'name username stageName avatar isVerified isDisabled location latitude longitude musicGenres musicRoles favoriteInstruments favoriteMaqamat', + }) + .sort({ createdAt: -1 }) + .limit(limit) + .exec(); + } + + async findTrendingPublicPosts(skip: number, limit: number): Promise { + return this.postModel + .find({ visibility: 'public', isDeleted: { $ne: true } }) + .populate({ path: 'authorId', select: 'name username stageName avatar isVerified isDisabled' }) + .sort({ likesCount: -1, commentsCount: -1, savesCount: -1, createdAt: -1 }) + .skip(skip) + .limit(limit) + .exec(); + } + + async count(filter: FilterQuery): Promise { + return this.postModel.countDocuments({ ...filter, isDeleted: { $ne: true } }).exec(); + } +} diff --git a/src/modules/feed/feed.service.ts b/src/modules/feed/feed.service.ts new file mode 100644 index 0000000..e204596 --- /dev/null +++ b/src/modules/feed/feed.service.ts @@ -0,0 +1,212 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { Types } from 'mongoose'; +import { decodeOffsetCursor, encodeOffsetCursor } from '../../common/utils/cursor.util'; +import { PostType } from '../../common/enums/post-type.enum'; +import { PostVisibility } from '../../common/enums/post-visibility.enum'; +import { UsersRepository } from '../users/users.repository'; +import { UserDocument } from '../users/schemas/user.schema'; +import { FeedQueryDto } from './dto/feed-query.dto'; +import { FeedRepository } from './feed.repository'; + +@Injectable() +export class FeedService { + constructor( + private readonly feedRepository: FeedRepository, + private readonly usersRepository: UsersRepository, + ) {} + + async getMyFeed(currentUserId: string, query: FeedQueryDto) { + const currentUser = await this.usersRepository.findById(currentUserId); + if (!currentUser) { + throw new NotFoundException('Current user not found'); + } + + const limit = query.limit ?? 20; + const cursorOffset = decodeOffsetCursor(query.cursor); + const page = query.page ?? 1; + const followingOnly = query.followingOnly ?? false; + const radiusKm = query.radiusKm ?? 30; + + const followingIds = await this.feedRepository.findFollowingIds(currentUserId); + const visibleAuthorIds = followingOnly ? [currentUserId, ...followingIds] : null; + + const filter: Record = { + $or: [ + { visibility: PostVisibility.PUBLIC }, + { authorId: new Types.ObjectId(currentUserId) }, + ], + }; + + if (visibleAuthorIds) { + filter.authorId = { $in: visibleAuthorIds.map((id) => new Types.ObjectId(id)) }; + } + + const candidates = await this.feedRepository.findCandidatePosts(filter, Math.max(limit * 12, 300)); + + const scored = candidates + .filter((post) => { + if (!query.preferredPostType) { + return true; + } + return post.postType === query.preferredPostType; + }) + .map((post) => ({ + post, + score: this.scorePost({ + currentUser, + currentUserId, + followingIds, + post, + preferredPostType: query.preferredPostType, + radiusKm, + }), + })) + .sort( + (a, b) => + b.score - a.score || + new Date((b.post as any).createdAt ?? 0).getTime() - new Date((a.post as any).createdAt ?? 0).getTime(), + ); + + const total = scored.length; + const skip = cursorOffset ?? (page - 1) * limit; + const items = scored.slice(skip, skip + limit).map((entry) => ({ + ...entry.post.toObject(), + feedScore: Number(entry.score.toFixed(3)), + })); + const nextOffset = skip + items.length; + const nextCursor = nextOffset < total ? encodeOffsetCursor(nextOffset) : null; + + return { + items, + page, + limit, + total, + totalPages: Math.ceil(total / limit) || 1, + nextCursor, + }; + } + + async getTrending(query: FeedQueryDto) { + const limit = query.limit ?? 20; + const cursorOffset = decodeOffsetCursor(query.cursor); + const page = query.page ?? 1; + const skip = cursorOffset ?? (page - 1) * limit; + + const [items, total] = await Promise.all([ + this.feedRepository.findTrendingPublicPosts(skip, limit), + this.feedRepository.count({ visibility: PostVisibility.PUBLIC }), + ]); + const nextOffset = skip + items.length; + const nextCursor = nextOffset < total ? encodeOffsetCursor(nextOffset) : null; + + return { + items, + page, + limit, + total, + totalPages: Math.ceil(total / limit) || 1, + nextCursor, + }; + } + + private scorePost(input: { + currentUser: UserDocument; + currentUserId: string; + followingIds: string[]; + post: any; + preferredPostType?: PostType; + radiusKm: number; + }): number { + const { currentUser, currentUserId, followingIds, post, preferredPostType, radiusKm } = input; + const author: any = post.authorId; + const authorId = typeof author === 'string' ? author : author?._id?.toString?.() ?? ''; + const isOwnPost = authorId === currentUserId; + const isFollowing = followingIds.includes(authorId); + + const ageMs = Date.now() - new Date(post.createdAt).getTime(); + const ageHours = ageMs / (1000 * 60 * 60); + const freshness = Math.max(0, 36 - ageHours); + + const engagement = post.likesCount * 3 + post.commentsCount * 4 + post.savesCount * 5; + const hashtagMatches = this.intersectionCount( + this.buildPreferenceTokens(currentUser), + (post.hashtags ?? []).map((x: string) => x.toLowerCase()), + ); + + const distanceKm = this.computeDistanceKm( + currentUser.latitude, + currentUser.longitude, + author?.latitude ?? null, + author?.longitude ?? null, + ); + const nearbyBoost = + typeof distanceKm === 'number' && distanceKm <= radiusKm ? Math.max(0, 25 - distanceKm / 2) : 0; + + let score = 0; + score += engagement; + score += freshness; + score += isOwnPost ? 10 : 0; + score += isFollowing ? 40 : 0; + score += post.postType === preferredPostType ? 18 : 0; + score += hashtagMatches * 9; + score += nearbyBoost; + score += author?.isVerified ? 8 : 0; + score += Math.min(20, Math.floor((author?.followersCount ?? 0) / 200)); + + return score; + } + + private buildPreferenceTokens(user: UserDocument): string[] { + const tokens = [ + ...(user.musicGenres ?? []), + ...(user.favoriteInstruments ?? []), + ...(user.favoriteMaqamat ?? []), + ...(user.musicRoles ?? []), + ] + .map((item) => item.trim().toLowerCase()) + .filter(Boolean); + + return Array.from(new Set(tokens)); + } + + private intersectionCount(a: string[], b: string[]): number { + if (!a.length || !b.length) { + return 0; + } + + const right = new Set(b); + let count = 0; + for (const item of a) { + if (right.has(item)) { + count += 1; + } + } + return count; + } + + private computeDistanceKm( + lat1: number | null | undefined, + lon1: number | null | undefined, + lat2: number | null | undefined, + lon2: number | null | undefined, + ): number | null { + if ( + typeof lat1 !== 'number' || + typeof lon1 !== 'number' || + typeof lat2 !== 'number' || + typeof lon2 !== 'number' + ) { + return null; + } + + const toRad = (deg: number) => (deg * Math.PI) / 180; + const earthKm = 6371; + const dLat = toRad(lat2 - lat1); + const dLon = toRad(lon2 - lon1); + const a = + Math.sin(dLat / 2) ** 2 + + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2; + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return earthKm * c; + } +} diff --git a/src/modules/follows/dto/toggle-follow.dto.ts b/src/modules/follows/dto/toggle-follow.dto.ts new file mode 100644 index 0000000..7ddd032 --- /dev/null +++ b/src/modules/follows/dto/toggle-follow.dto.ts @@ -0,0 +1,6 @@ +import { IsMongoId } from 'class-validator'; + +export class ToggleFollowDto { + @IsMongoId() + targetUserId!: string; +} diff --git a/src/modules/follows/follows.controller.ts b/src/modules/follows/follows.controller.ts new file mode 100644 index 0000000..b9ba693 --- /dev/null +++ b/src/modules/follows/follows.controller.ts @@ -0,0 +1,52 @@ +import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { CurrentUser } from '../../common/decorators/current-user.decorator'; +import { Throttle } from '../../common/decorators/throttle.decorator'; +import { PaginationQueryDto } from '../../common/dto/pagination-query.dto'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { JwtPayload } from '../../common/interfaces/jwt-payload.interface'; +import { ToggleFollowDto } from './dto/toggle-follow.dto'; +import { FollowsService } from './follows.service'; + +@ApiTags('Follows') +@Controller('follows') +export class FollowsController { + constructor(private readonly followsService: FollowsService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post('toggle') + @Throttle(30, 60_000) + async toggleFollow(@CurrentUser() user: JwtPayload, @Body() dto: ToggleFollowDto) { + return this.followsService.toggleFollow(user.sub, dto); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('followers/:userId') + async followers(@Param('userId') userId: string, @Query() query: PaginationQueryDto) { + return this.followsService.getFollowers(userId, query.page, query.limit); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('following/:userId') + async following(@Param('userId') userId: string, @Query() query: PaginationQueryDto) { + return this.followsService.getFollowing(userId, query.page, query.limit); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('status/:targetUserId') + async status(@CurrentUser() user: JwtPayload, @Param('targetUserId') targetUserId: string) { + return this.followsService.getFollowStatus(user.sub, targetUserId); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('suggestions') + @Throttle(60, 60_000) + async suggestions(@CurrentUser() user: JwtPayload, @Query() query: PaginationQueryDto) { + return this.followsService.getSuggestions(user.sub, query.page, query.limit); + } +} diff --git a/src/modules/follows/follows.module.ts b/src/modules/follows/follows.module.ts new file mode 100644 index 0000000..1d249c3 --- /dev/null +++ b/src/modules/follows/follows.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { OutboxModule } from '../outbox/outbox.module'; +import { UsersModule } from '../users/users.module'; +import { FollowsController } from './follows.controller'; +import { FollowsService } from './follows.service'; +import { FollowsRepository } from './follows.repository'; +import { Follow, FollowSchema } from './schemas/follow.schema'; + +@Module({ + imports: [ + UsersModule, + OutboxModule, + MongooseModule.forFeature([ + { + name: Follow.name, + schema: FollowSchema, + }, + ]), + ], + controllers: [FollowsController], + providers: [FollowsService, FollowsRepository], + exports: [FollowsService], +}) +export class FollowsModule {} diff --git a/src/modules/follows/follows.repository.ts b/src/modules/follows/follows.repository.ts new file mode 100644 index 0000000..69e71c4 --- /dev/null +++ b/src/modules/follows/follows.repository.ts @@ -0,0 +1,65 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { ClientSession, FilterQuery, Model, Types } from 'mongoose'; +import { Follow, FollowDocument } from './schemas/follow.schema'; + +@Injectable() +export class FollowsRepository { + constructor(@InjectModel(Follow.name) private readonly followModel: Model) {} + + async findOne(followerId: string, followingId: string): Promise { + return this.followModel + .findOne({ + followerId: new Types.ObjectId(followerId), + followingId: new Types.ObjectId(followingId), + }) + .exec(); + } + + async create( + followerId: string, + followingId: string, + session?: ClientSession, + ): Promise { + const [follow] = await this.followModel.create( + [ + { + followerId: new Types.ObjectId(followerId), + followingId: new Types.ObjectId(followingId), + }, + ], + { session }, + ); + + return follow; + } + + async deleteById(id: string, session?: ClientSession): Promise { + await this.followModel.findByIdAndDelete(id, { session }).exec(); + } + + async findMany(filter: FilterQuery, skip: number, limit: number): Promise { + return this.followModel + .find(filter) + .populate({ path: 'followerId', select: 'name username stageName avatar isVerified isDisabled' }) + .populate({ path: 'followingId', select: 'name username stageName avatar isVerified isDisabled' }) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit) + .exec(); + } + + async count(filter: FilterQuery): Promise { + return this.followModel.countDocuments(filter).exec(); + } + + async findFollowingIds(followerId: string): Promise { + const rows = await this.followModel + .find({ followerId: new Types.ObjectId(followerId) }) + .select({ followingId: 1 }) + .lean() + .exec(); + + return rows.map((row) => row.followingId.toString()); + } +} diff --git a/src/modules/follows/follows.service.spec.ts b/src/modules/follows/follows.service.spec.ts new file mode 100644 index 0000000..016a4c7 --- /dev/null +++ b/src/modules/follows/follows.service.spec.ts @@ -0,0 +1,42 @@ +import { FollowsService } from './follows.service'; + +describe('FollowsService', () => { + it('keeps follow successful even if notification creation fails and resyncs counters', async () => { + const currentUserId = '507f1f77bcf86cd799439011'; + const targetUserId = '507f191e810c19729de860ea'; + + const followsRepository = { + findOne: jest.fn().mockResolvedValue(null), + create: jest.fn().mockResolvedValue({ id: 'follow-1' }), + count: jest + .fn() + .mockResolvedValueOnce(6) + .mockResolvedValueOnce(14), + }; + const usersRepository = { + findById: jest.fn().mockResolvedValue({ id: targetUserId }), + setFollowingCount: jest.fn().mockResolvedValue(undefined), + setFollowersCount: jest.fn().mockResolvedValue(undefined), + }; + const outboxService = { + enqueueFollowNotification: jest.fn().mockRejectedValue(new Error('socket down')), + }; + + const service = new FollowsService( + followsRepository as any, + usersRepository as any, + outboxService as any, + ); + + await expect(service.toggleFollow(currentUserId, { targetUserId })).resolves.toEqual({ + following: true, + }); + expect(usersRepository.setFollowingCount).toHaveBeenCalledWith(currentUserId, 6); + expect(usersRepository.setFollowersCount).toHaveBeenCalledWith(targetUserId, 14); + expect(outboxService.enqueueFollowNotification).toHaveBeenCalledWith( + currentUserId, + targetUserId, + 'follow-1', + ); + }); +}); diff --git a/src/modules/follows/follows.service.ts b/src/modules/follows/follows.service.ts new file mode 100644 index 0000000..3649d49 --- /dev/null +++ b/src/modules/follows/follows.service.ts @@ -0,0 +1,223 @@ +import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { Types } from 'mongoose'; +import { OutboxService } from '../outbox/outbox.service'; +import { UsersRepository } from '../users/users.repository'; +import { UserDocument } from '../users/schemas/user.schema'; +import { ToggleFollowDto } from './dto/toggle-follow.dto'; +import { FollowsRepository } from './follows.repository'; + +@Injectable() +export class FollowsService { + private readonly logger = new Logger(FollowsService.name); + + constructor( + private readonly followsRepository: FollowsRepository, + private readonly usersRepository: UsersRepository, + private readonly outboxService: OutboxService, + ) {} + + async toggleFollow(currentUserId: string, dto: ToggleFollowDto) { + const targetUserId = dto.targetUserId; + + if (!Types.ObjectId.isValid(targetUserId)) { + throw new BadRequestException('Invalid target user id'); + } + + if (currentUserId === targetUserId) { + throw new BadRequestException('You cannot follow yourself'); + } + + const targetUser = await this.usersRepository.findById(targetUserId); + if (!targetUser) { + throw new NotFoundException('Target user not found'); + } + + const existing = await this.followsRepository.findOne(currentUserId, targetUserId); + + if (existing) { + await this.followsRepository.deleteById(existing.id); + await this.syncFollowCounts(currentUserId, targetUserId); + return { following: false }; + } + + const follow = await this.followsRepository.create(currentUserId, targetUserId); + await this.syncFollowCounts(currentUserId, targetUserId); + + try { + await this.outboxService.enqueueFollowNotification(currentUserId, targetUserId, follow.id); + } catch (error) { + this.logger.warn( + `Follow notification failed for actor=${currentUserId} recipient=${targetUserId}: ${ + error instanceof Error ? error.message : 'unknown error' + }`, + ); + } + + return { following: true }; + } + + async getFollowers(userId: string, page = 1, limit = 20) { + const skip = (page - 1) * limit; + const [items, total] = await Promise.all([ + this.followsRepository.findMany({ followingId: userId }, skip, limit), + this.followsRepository.count({ followingId: userId }), + ]); + + return { + items, + page, + limit, + total, + totalPages: Math.ceil(total / limit) || 1, + }; + } + + async getFollowing(userId: string, page = 1, limit = 20) { + const skip = (page - 1) * limit; + const [items, total] = await Promise.all([ + this.followsRepository.findMany({ followerId: userId }, skip, limit), + this.followsRepository.count({ followerId: userId }), + ]); + + return { + items, + page, + limit, + total, + totalPages: Math.ceil(total / limit) || 1, + }; + } + + async getFollowStatus(currentUserId: string, targetUserId: string) { + if (!Types.ObjectId.isValid(targetUserId)) { + throw new BadRequestException('Invalid target user id'); + } + + if (currentUserId === targetUserId) { + return { following: false, targetUserId }; + } + + const existing = await this.followsRepository.findOne(currentUserId, targetUserId); + return { + following: !!existing, + targetUserId, + }; + } + + async getSuggestions(currentUserId: string, page = 1, limit = 20) { + const currentUser = await this.usersRepository.findById(currentUserId); + if (!currentUser) { + throw new NotFoundException('Current user not found'); + } + + const followingIds = await this.followsRepository.findFollowingIds(currentUserId); + const excludedIds = new Set([currentUserId, ...followingIds]); + + const candidates = await this.usersRepository.findSuggestionCandidates( + { + _id: { $nin: Array.from(excludedIds).map((id) => new Types.ObjectId(id)) }, + isDisabled: false, + }, + 500, + ); + + const ranked = candidates + .map((candidate) => ({ + user: candidate, + score: this.calculateSuggestionScore(currentUser, candidate), + })) + .sort((a, b) => b.score - a.score || b.user.followersCount - a.user.followersCount); + + const total = ranked.length; + const skip = (page - 1) * limit; + const items = ranked.slice(skip, skip + limit).map((entry) => ({ + user: entry.user, + score: entry.score, + reasons: this.buildSuggestionReasons(currentUser, entry.user), + })); + + return { + items, + page, + limit, + total, + totalPages: Math.ceil(total / limit) || 1, + }; + } + + private calculateSuggestionScore(currentUser: UserDocument, candidate: UserDocument): number { + const sharedRoles = this.intersectionCount(currentUser.musicRoles, candidate.musicRoles); + const sharedGenres = this.intersectionCount(currentUser.musicGenres, candidate.musicGenres); + const sharedInstruments = this.intersectionCount( + currentUser.favoriteInstruments, + candidate.favoriteInstruments, + ); + const sharedMaqamat = this.intersectionCount(currentUser.favoriteMaqamat, candidate.favoriteMaqamat); + const sameLocation = this.normalize(currentUser.location) === this.normalize(candidate.location); + + let score = 0; + score += sharedRoles * 15; + score += sharedGenres * 8; + score += sharedInstruments * 6; + score += sharedMaqamat * 6; + score += sameLocation ? 20 : 0; + score += candidate.isVerified ? 25 : 0; + score += Math.min(25, Math.floor(candidate.followersCount / 100)); + score += Math.random(); + + return score; + } + + private buildSuggestionReasons(currentUser: UserDocument, candidate: UserDocument): string[] { + const reasons: string[] = []; + + if (this.intersectionCount(currentUser.musicRoles, candidate.musicRoles) > 0) { + reasons.push('shared_music_roles'); + } + if (this.intersectionCount(currentUser.musicGenres, candidate.musicGenres) > 0) { + reasons.push('shared_genres'); + } + if (this.normalize(currentUser.location) === this.normalize(candidate.location)) { + reasons.push('same_location'); + } + if (candidate.isVerified) { + reasons.push('verified_account'); + } + if (candidate.followersCount > 500) { + reasons.push('popular_creator'); + } + + return reasons; + } + + private normalize(value?: string): string { + return (value ?? '').trim().toLowerCase(); + } + + private intersectionCount(a: string[] = [], b: string[] = []): number { + if (!a.length || !b.length) { + return 0; + } + + const right = new Set(b.map((item) => item.toLowerCase())); + let count = 0; + for (const item of a) { + if (right.has(item.toLowerCase())) { + count += 1; + } + } + return count; + } + + private async syncFollowCounts(currentUserId: string, targetUserId: string): Promise { + const [followingCount, followersCount] = await Promise.all([ + this.followsRepository.count({ followerId: currentUserId }), + this.followsRepository.count({ followingId: targetUserId }), + ]); + + await Promise.all([ + this.usersRepository.setFollowingCount(currentUserId, followingCount), + this.usersRepository.setFollowersCount(targetUserId, followersCount), + ]); + } +} diff --git a/src/modules/follows/schemas/follow.schema.ts b/src/modules/follows/schemas/follow.schema.ts new file mode 100644 index 0000000..cdbe9ef --- /dev/null +++ b/src/modules/follows/schemas/follow.schema.ts @@ -0,0 +1,17 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { HydratedDocument, Types } from 'mongoose'; +import { User } from '../../users/schemas/user.schema'; + +export type FollowDocument = HydratedDocument; + +@Schema({ timestamps: true, versionKey: false }) +export class Follow { + @Prop({ type: Types.ObjectId, ref: User.name, required: true, index: true }) + followerId!: Types.ObjectId; + + @Prop({ type: Types.ObjectId, ref: User.name, required: true, index: true }) + followingId!: Types.ObjectId; +} + +export const FollowSchema = SchemaFactory.createForClass(Follow); +FollowSchema.index({ followerId: 1, followingId: 1 }, { unique: true }); diff --git a/src/modules/likes/dto/toggle-like.dto.ts b/src/modules/likes/dto/toggle-like.dto.ts new file mode 100644 index 0000000..cefe6e3 --- /dev/null +++ b/src/modules/likes/dto/toggle-like.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsIn, IsMongoId } from 'class-validator'; + +export class ToggleLikeDto { + @ApiProperty() + @IsMongoId() + targetId!: string; + + @ApiProperty({ enum: ['post', 'comment'] }) + @IsIn(['post', 'comment']) + targetType!: 'post' | 'comment'; +} diff --git a/src/modules/likes/likes.controller.ts b/src/modules/likes/likes.controller.ts new file mode 100644 index 0000000..6d374e9 --- /dev/null +++ b/src/modules/likes/likes.controller.ts @@ -0,0 +1,43 @@ +import { Body, Controller, Delete, Get, Param, Post, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { CurrentUser } from '../../common/decorators/current-user.decorator'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { JwtPayload } from '../../common/interfaces/jwt-payload.interface'; +import { ToggleLikeDto } from './dto/toggle-like.dto'; +import { LikesService } from './likes.service'; + +@ApiTags('Likes') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard) +@Controller('likes') +export class LikesController { + constructor(private readonly likesService: LikesService) {} + + @Post() + async like(@CurrentUser() user: JwtPayload, @Body() dto: ToggleLikeDto) { + return this.likesService.like(user.sub, dto); + } + + @Delete(':targetType/:targetId') + async unlike( + @CurrentUser() user: JwtPayload, + @Param('targetId') targetId: string, + @Param('targetType') targetType: 'post' | 'comment', + ) { + return this.likesService.unlike(user.sub, { targetId, targetType }); + } + + @Get('status/:targetType/:targetId') + async getStatus( + @CurrentUser() user: JwtPayload, + @Param('targetId') targetId: string, + @Param('targetType') targetType: 'post' | 'comment', + ) { + return this.likesService.getStatus(user.sub, { targetId, targetType }); + } + + @Post('toggle') + async toggle(@CurrentUser() user: JwtPayload, @Body() dto: ToggleLikeDto) { + return this.likesService.toggle(user.sub, dto); + } +} diff --git a/src/modules/likes/likes.module.ts b/src/modules/likes/likes.module.ts new file mode 100644 index 0000000..ceb95e0 --- /dev/null +++ b/src/modules/likes/likes.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { CommentsModule } from '../comments/comments.module'; +import { PostsModule } from '../posts/posts.module'; +import { Like, LikeSchema } from './schemas/like.schema'; +import { LikesController } from './likes.controller'; +import { LikesRepository } from './likes.repository'; +import { LikesService } from './likes.service'; + +@Module({ + imports: [ + MongooseModule.forFeature([{ name: Like.name, schema: LikeSchema }]), + PostsModule, + CommentsModule, + ], + controllers: [LikesController], + providers: [LikesService, LikesRepository], + exports: [LikesService], +}) +export class LikesModule {} diff --git a/src/modules/likes/likes.repository.ts b/src/modules/likes/likes.repository.ts new file mode 100644 index 0000000..fb61b4d --- /dev/null +++ b/src/modules/likes/likes.repository.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model, Types } from 'mongoose'; +import { Like, LikeDocument } from './schemas/like.schema'; + +@Injectable() +export class LikesRepository { + constructor(@InjectModel(Like.name) private readonly likeModel: Model) {} + + async findOne(userId: string, targetId: string, targetType: 'post' | 'comment'): Promise { + return this.likeModel + .findOne({ + userId: new Types.ObjectId(userId), + targetId: new Types.ObjectId(targetId), + targetType, + }) + .exec(); + } + + async create(userId: string, targetId: string, targetType: 'post' | 'comment'): Promise { + return this.likeModel.create({ + userId: new Types.ObjectId(userId), + targetId: new Types.ObjectId(targetId), + targetType, + }); + } + + async deleteById(id: string): Promise { + await this.likeModel.findByIdAndDelete(id).exec(); + } +} diff --git a/src/modules/likes/likes.service.spec.ts b/src/modules/likes/likes.service.spec.ts new file mode 100644 index 0000000..4e1a908 --- /dev/null +++ b/src/modules/likes/likes.service.spec.ts @@ -0,0 +1,30 @@ +import { LikesService } from './likes.service'; + +describe('LikesService', () => { + it('returns liked false from status when target post no longer exists', async () => { + const likesRepository = { + findOne: jest.fn(), + }; + const postsRepository = { + findById: jest.fn().mockResolvedValue(null), + }; + const commentsRepository = { + findById: jest.fn(), + }; + + const service = new LikesService( + likesRepository as any, + postsRepository as any, + commentsRepository as any, + ); + + await expect( + service.getStatus('user-1', { targetId: '507f1f77bcf86cd799439011', targetType: 'post' }), + ).resolves.toEqual({ + liked: false, + targetId: '507f1f77bcf86cd799439011', + targetType: 'post', + }); + expect(likesRepository.findOne).not.toHaveBeenCalled(); + }); +}); diff --git a/src/modules/likes/likes.service.ts b/src/modules/likes/likes.service.ts new file mode 100644 index 0000000..3ca252f --- /dev/null +++ b/src/modules/likes/likes.service.ts @@ -0,0 +1,78 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { CommentsRepository } from '../comments/comments.repository'; +import { PostsRepository } from '../posts/posts.repository'; +import { LikesRepository } from './likes.repository'; +import { ToggleLikeDto } from './dto/toggle-like.dto'; + +@Injectable() +export class LikesService { + constructor( + private readonly likesRepository: LikesRepository, + private readonly postsRepository: PostsRepository, + private readonly commentsRepository: CommentsRepository, + ) {} + + async toggle(userId: string, dto: ToggleLikeDto): Promise<{ liked: boolean; targetId: string; targetType: string }> { + const existing = await this.likesRepository.findOne(userId, dto.targetId, dto.targetType); + return existing ? this.unlike(userId, dto) : this.like(userId, dto); + } + + async like(userId: string, dto: ToggleLikeDto): Promise<{ liked: boolean; targetId: string; targetType: string }> { + await this.assertTargetExists(dto); + + const existing = await this.likesRepository.findOne(userId, dto.targetId, dto.targetType); + if (existing) { + return { liked: true, targetId: dto.targetId, targetType: dto.targetType }; + } + + await this.likesRepository.create(userId, dto.targetId, dto.targetType); + if (dto.targetType === 'post') { + await this.postsRepository.incrementLikesCount(dto.targetId, 1); + } + + return { liked: true, targetId: dto.targetId, targetType: dto.targetType }; + } + + async unlike(userId: string, dto: ToggleLikeDto): Promise<{ liked: boolean; targetId: string; targetType: string }> { + await this.assertTargetExists(dto); + + const existing = await this.likesRepository.findOne(userId, dto.targetId, dto.targetType); + if (!existing) { + return { liked: false, targetId: dto.targetId, targetType: dto.targetType }; + } + + await this.likesRepository.deleteById(existing.id); + if (dto.targetType === 'post') { + await this.postsRepository.incrementLikesCount(dto.targetId, -1); + } + + return { liked: false, targetId: dto.targetId, targetType: dto.targetType }; + } + + async getStatus(userId: string, dto: ToggleLikeDto): Promise<{ liked: boolean; targetId: string; targetType: string }> { + const targetExists = await this.targetExists(dto); + if (!targetExists) { + return { liked: false, targetId: dto.targetId, targetType: dto.targetType }; + } + + const existing = await this.likesRepository.findOne(userId, dto.targetId, dto.targetType); + return { liked: !!existing, targetId: dto.targetId, targetType: dto.targetType }; + } + + private async assertTargetExists(dto: ToggleLikeDto): Promise { + const targetExists = await this.targetExists(dto); + if (!targetExists) { + throw new NotFoundException(dto.targetType === 'post' ? 'Post not found' : 'Comment not found'); + } + } + + private async targetExists(dto: ToggleLikeDto): Promise { + if (dto.targetType === 'post') { + const post = await this.postsRepository.findById(dto.targetId); + return !!post; + } + + const comment = await this.commentsRepository.findById(dto.targetId); + return !!comment; + } +} diff --git a/src/modules/likes/schemas/like.schema.ts b/src/modules/likes/schemas/like.schema.ts new file mode 100644 index 0000000..95a17db --- /dev/null +++ b/src/modules/likes/schemas/like.schema.ts @@ -0,0 +1,19 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { HydratedDocument, Types } from 'mongoose'; + +export type LikeDocument = HydratedDocument; + +@Schema({ timestamps: true, versionKey: false }) +export class Like { + @Prop({ type: Types.ObjectId, required: true, index: true }) + userId!: Types.ObjectId; + + @Prop({ type: Types.ObjectId, required: true, index: true }) + targetId!: Types.ObjectId; + + @Prop({ required: true, enum: ['post', 'comment'] }) + targetType!: 'post' | 'comment'; +} + +export const LikeSchema = SchemaFactory.createForClass(Like); +LikeSchema.index({ userId: 1, targetId: 1, targetType: 1 }, { unique: true }); diff --git a/src/modules/marketplace/dto/create-instrument.dto.ts b/src/modules/marketplace/dto/create-instrument.dto.ts new file mode 100644 index 0000000..4770a18 --- /dev/null +++ b/src/modules/marketplace/dto/create-instrument.dto.ts @@ -0,0 +1,50 @@ +import { Type } from 'class-transformer'; +import { + ArrayMaxSize, + IsArray, + IsBoolean, + IsNotEmpty, + IsNumber, + IsOptional, + IsString, + MaxLength, + Min, +} from 'class-validator'; + +export class CreateInstrumentDto { + @IsString() + @IsNotEmpty() + @MaxLength(120) + title!: string; + + @IsOptional() + @IsString() + @MaxLength(2000) + description?: string; + + @Type(() => Number) + @IsNumber() + @Min(0) + price!: number; + + @IsOptional() + @IsString() + @MaxLength(8) + currency?: string; + + @Type(() => Number) + @IsNumber() + @Min(0) + quantity!: number; + + @IsOptional() + @IsArray() + @ArrayMaxSize(5) + @IsString({ each: true }) + imageUrls?: string[]; + + @IsOptional() + @Type(() => Boolean) + @IsBoolean() + isActive?: boolean; +} diff --git a/src/modules/marketplace/dto/instrument-query.dto.ts b/src/modules/marketplace/dto/instrument-query.dto.ts new file mode 100644 index 0000000..fb05036 --- /dev/null +++ b/src/modules/marketplace/dto/instrument-query.dto.ts @@ -0,0 +1,33 @@ +import { Type } from 'class-transformer'; +import { IsBoolean, IsNumber, IsOptional, IsString, Max, Min } from 'class-validator'; +import { PaginationQueryDto } from '../../../common/dto/pagination-query.dto'; + +export class InstrumentQueryDto extends PaginationQueryDto { + @IsOptional() + @IsString() + q?: string; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + minPrice?: number; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + maxPrice?: number; + + @IsOptional() + @Type(() => Boolean) + @IsBoolean() + isActive?: boolean; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(1) + @Max(200) + limit?: number; +} diff --git a/src/modules/marketplace/dto/update-instrument.dto.ts b/src/modules/marketplace/dto/update-instrument.dto.ts new file mode 100644 index 0000000..b1a9886 --- /dev/null +++ b/src/modules/marketplace/dto/update-instrument.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateInstrumentDto } from './create-instrument.dto'; + +export class UpdateInstrumentDto extends PartialType(CreateInstrumentDto) {} diff --git a/src/modules/marketplace/marketplace.controller.ts b/src/modules/marketplace/marketplace.controller.ts new file mode 100644 index 0000000..83beb44 --- /dev/null +++ b/src/modules/marketplace/marketplace.controller.ts @@ -0,0 +1,69 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { CurrentUser } from '../../common/decorators/current-user.decorator'; +import { Roles } from '../../common/decorators/roles.decorator'; +import { Throttle } from '../../common/decorators/throttle.decorator'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { RolesGuard } from '../../common/guards/roles.guard'; +import { UserRole } from '../../common/enums/user-role.enum'; +import { JwtPayload } from '../../common/interfaces/jwt-payload.interface'; +import { CreateInstrumentDto } from './dto/create-instrument.dto'; +import { InstrumentQueryDto } from './dto/instrument-query.dto'; +import { UpdateInstrumentDto } from './dto/update-instrument.dto'; +import { MarketplaceService } from './marketplace.service'; + +@ApiTags('Marketplace') +@Controller('marketplace') +export class MarketplaceController { + constructor(private readonly marketplaceService: MarketplaceService) {} + + @Get('instruments') + async listPublic(@Query() query: InstrumentQueryDto) { + return this.marketplaceService.getPublic(query); + } + + @Get('instruments/:id') + async findOne(@Param('id') instrumentId: string) { + return this.marketplaceService.findById(instrumentId); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(UserRole.ADMIN) + @Post('admin/instruments') + @Throttle(40, 60_000) + async createByAdmin(@CurrentUser() user: JwtPayload, @Body() dto: CreateInstrumentDto) { + return this.marketplaceService.createByAdmin(user.sub, dto); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(UserRole.ADMIN) + @Patch('admin/instruments/:id') + @Throttle(60, 60_000) + async updateByAdmin( + @CurrentUser() user: JwtPayload, + @Param('id') instrumentId: string, + @Body() dto: UpdateInstrumentDto, + ) { + return this.marketplaceService.updateByAdmin(user.sub, instrumentId, dto); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(UserRole.ADMIN) + @Delete('admin/instruments/:id') + @Throttle(40, 60_000) + async removeByAdmin(@CurrentUser() user: JwtPayload, @Param('id') instrumentId: string) { + await this.marketplaceService.removeByAdmin(user.sub, instrumentId); + return { success: true }; + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(UserRole.ADMIN) + @Get('admin/instruments/me') + async myInstruments(@CurrentUser() user: JwtPayload, @Query() query: InstrumentQueryDto) { + return this.marketplaceService.getMine(user.sub, query); + } +} diff --git a/src/modules/marketplace/marketplace.module.ts b/src/modules/marketplace/marketplace.module.ts new file mode 100644 index 0000000..7a888af --- /dev/null +++ b/src/modules/marketplace/marketplace.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { UsersModule } from '../users/users.module'; +import { MarketplaceController } from './marketplace.controller'; +import { MarketplaceRepository } from './marketplace.repository'; +import { MarketplaceService } from './marketplace.service'; +import { Instrument, InstrumentSchema } from './schemas/instrument.schema'; + +@Module({ + imports: [ + UsersModule, + MongooseModule.forFeature([{ name: Instrument.name, schema: InstrumentSchema }]), + ], + controllers: [MarketplaceController], + providers: [MarketplaceService, MarketplaceRepository], + exports: [MarketplaceService], +}) +export class MarketplaceModule {} diff --git a/src/modules/marketplace/marketplace.repository.ts b/src/modules/marketplace/marketplace.repository.ts new file mode 100644 index 0000000..6cea0df --- /dev/null +++ b/src/modules/marketplace/marketplace.repository.ts @@ -0,0 +1,135 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { FilterQuery, Model, Types, UpdateQuery } from 'mongoose'; +import { Instrument, InstrumentDocument } from './schemas/instrument.schema'; + +@Injectable() +export class MarketplaceRepository { + constructor( + @InjectModel(Instrument.name) + private readonly instrumentModel: Model, + ) {} + + async create(ownerAdminId: string, payload: Partial): Promise { + return this.instrumentModel.create({ + ...payload, + ownerAdminId: new Types.ObjectId(ownerAdminId), + }); + } + + async findById(instrumentId: string): Promise { + if (!Types.ObjectId.isValid(instrumentId)) { + return null; + } + return this.instrumentModel + .findById(instrumentId) + .populate({ path: 'ownerAdminId', select: 'name username email avatar isDisabled' }) + .exec(); + } + + async updateById( + instrumentId: string, + payload: UpdateQuery, + ): Promise { + if (!Types.ObjectId.isValid(instrumentId)) { + return null; + } + + return this.instrumentModel + .findByIdAndUpdate(instrumentId, payload, { new: true }) + .populate({ path: 'ownerAdminId', select: 'name username email avatar isDisabled' }) + .exec(); + } + + async deleteById(instrumentId: string): Promise { + if (!Types.ObjectId.isValid(instrumentId)) { + return null; + } + return this.instrumentModel.findByIdAndDelete(instrumentId).exec(); + } + + async findMany( + filter: FilterQuery, + skip: number, + limit: number, + ): Promise { + return this.instrumentModel + .find(filter) + .populate({ path: 'ownerAdminId', select: 'name username email avatar isDisabled' }) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit) + .exec(); + } + + async findManyPublic( + filter: FilterQuery, + skip: number, + limit: number, + ): Promise[]> { + return this.instrumentModel + .aggregate([ + { $match: filter }, + { + $lookup: { + from: 'users', + localField: 'ownerAdminId', + foreignField: '_id', + as: 'ownerAdmin', + }, + }, + { $unwind: '$ownerAdmin' }, + { $match: { 'ownerAdmin.isDisabled': false } }, + { $sort: { createdAt: -1 } }, + { $skip: skip }, + { $limit: limit }, + { + $project: { + _id: 1, + title: 1, + description: 1, + price: 1, + currency: 1, + quantity: 1, + imageUrls: 1, + isActive: 1, + createdAt: 1, + updatedAt: 1, + ownerAdminId: { + _id: '$ownerAdmin._id', + name: '$ownerAdmin.name', + username: '$ownerAdmin.username', + email: '$ownerAdmin.email', + avatar: '$ownerAdmin.avatar', + }, + }, + }, + ]) + .exec(); + } + + async countPublic(filter: FilterQuery): Promise { + const rows = await this.instrumentModel + .aggregate([ + { $match: filter }, + { + $lookup: { + from: 'users', + localField: 'ownerAdminId', + foreignField: '_id', + as: 'ownerAdmin', + }, + }, + { $unwind: '$ownerAdmin' }, + { $match: { 'ownerAdmin.isDisabled': false } }, + { $count: 'count' }, + ]) + .exec(); + + return rows[0]?.count ?? 0; + } + + async count(filter: FilterQuery): Promise { + return this.instrumentModel.countDocuments(filter).exec(); + } +} diff --git a/src/modules/marketplace/marketplace.service.ts b/src/modules/marketplace/marketplace.service.ts new file mode 100644 index 0000000..cc65ad1 --- /dev/null +++ b/src/modules/marketplace/marketplace.service.ts @@ -0,0 +1,168 @@ +import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common'; +import { FilterQuery, Types } from 'mongoose'; +import { UserRole } from '../../common/enums/user-role.enum'; +import { UsersRepository } from '../users/users.repository'; +import { CreateInstrumentDto } from './dto/create-instrument.dto'; +import { InstrumentQueryDto } from './dto/instrument-query.dto'; +import { UpdateInstrumentDto } from './dto/update-instrument.dto'; +import { MarketplaceRepository } from './marketplace.repository'; +import { InstrumentDocument } from './schemas/instrument.schema'; + +@Injectable() +export class MarketplaceService { + constructor( + private readonly marketplaceRepository: MarketplaceRepository, + private readonly usersRepository: UsersRepository, + ) {} + + async createByAdmin(adminUserId: string, dto: CreateInstrumentDto): Promise { + await this.assertAdminRole(adminUserId); + this.assertImageUrlsCount(dto.imageUrls); + return this.marketplaceRepository.create(adminUserId, { + ...dto, + currency: (dto.currency ?? 'SAR').toUpperCase(), + description: dto.description ?? '', + imageUrls: dto.imageUrls ?? [], + isActive: dto.isActive ?? true, + }); + } + + async updateByAdmin( + adminUserId: string, + instrumentId: string, + dto: UpdateInstrumentDto, + ): Promise { + await this.assertAdminRole(adminUserId); + this.assertImageUrlsCount(dto.imageUrls); + const existing = await this.marketplaceRepository.findById(instrumentId); + if (!existing) { + throw new NotFoundException('Instrument not found'); + } + if (existing.ownerAdminId.toString() !== adminUserId) { + throw new ForbiddenException('You can update only your instruments'); + } + + const updated = await this.marketplaceRepository.updateById(instrumentId, { + ...dto, + ...(dto.currency ? { currency: dto.currency.toUpperCase() } : {}), + }); + if (!updated) { + throw new NotFoundException('Instrument not found'); + } + return updated; + } + + async removeByAdmin(adminUserId: string, instrumentId: string): Promise { + await this.assertAdminRole(adminUserId); + const existing = await this.marketplaceRepository.findById(instrumentId); + if (!existing) { + throw new NotFoundException('Instrument not found'); + } + if (existing.ownerAdminId.toString() !== adminUserId) { + throw new ForbiddenException('You can delete only your instruments'); + } + await this.marketplaceRepository.deleteById(instrumentId); + } + + async getMine(adminUserId: string, query: InstrumentQueryDto) { + await this.assertAdminRole(adminUserId); + const page = query.page ?? 1; + const limit = query.limit ?? 20; + const skip = (page - 1) * limit; + + const filter = this.buildFilter(query); + filter.ownerAdminId = new Types.ObjectId(adminUserId); + + const [items, total] = await Promise.all([ + this.marketplaceRepository.findManyPublic(filter, skip, limit), + this.marketplaceRepository.countPublic(filter), + ]); + + return { + items, + page, + limit, + total, + totalPages: Math.ceil(total / limit) || 1, + }; + } + + async getPublic(query: InstrumentQueryDto) { + const page = query.page ?? 1; + const limit = query.limit ?? 20; + const skip = (page - 1) * limit; + + const filter = this.buildFilter(query); + if (typeof query.isActive === 'undefined') { + filter.isActive = true; + } + + const [items, total] = await Promise.all([ + this.marketplaceRepository.findMany(filter, skip, limit), + this.marketplaceRepository.count(filter), + ]); + + return { + items, + page, + limit, + total, + totalPages: Math.ceil(total / limit) || 1, + }; + } + + async findById(instrumentId: string) { + const item = await this.marketplaceRepository.findById(instrumentId); + if (!item) { + throw new NotFoundException('Instrument not found'); + } + const owner = item.ownerAdminId as unknown as { isDisabled?: boolean } | undefined; + if (owner?.isDisabled) { + throw new NotFoundException('Instrument not found'); + } + return item; + } + + private buildFilter(query: InstrumentQueryDto): FilterQuery { + const filter: FilterQuery = {}; + + if (query.q) { + filter.$or = [ + { title: { $regex: query.q, $options: 'i' } }, + { description: { $regex: query.q, $options: 'i' } }, + ]; + } + + if (typeof query.minPrice === 'number' || typeof query.maxPrice === 'number') { + filter.price = {}; + if (typeof query.minPrice === 'number') { + (filter.price as Record).$gte = query.minPrice; + } + if (typeof query.maxPrice === 'number') { + (filter.price as Record).$lte = query.maxPrice; + } + } + + if (typeof query.isActive === 'boolean') { + filter.isActive = query.isActive; + } + + return filter; + } + + private async assertAdminRole(userId: string): Promise { + const user = await this.usersRepository.findById(userId); + if (!user) { + throw new NotFoundException('User not found'); + } + if (user.role !== UserRole.ADMIN) { + throw new ForbiddenException('Admin role required'); + } + } + + private assertImageUrlsCount(imageUrls?: string[]): void { + if (imageUrls && imageUrls.length > 5) { + throw new BadRequestException('You can upload up to 5 images only'); + } + } +} diff --git a/src/modules/marketplace/schemas/instrument.schema.ts b/src/modules/marketplace/schemas/instrument.schema.ts new file mode 100644 index 0000000..5fd906b --- /dev/null +++ b/src/modules/marketplace/schemas/instrument.schema.ts @@ -0,0 +1,37 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { HydratedDocument, Types } from 'mongoose'; +import { User } from '../../users/schemas/user.schema'; + +export type InstrumentDocument = HydratedDocument; + +@Schema({ timestamps: true, versionKey: false }) +export class Instrument { + @Prop({ type: Types.ObjectId, ref: User.name, required: true, index: true }) + ownerAdminId!: Types.ObjectId; + + @Prop({ required: true, trim: true, maxlength: 120, index: true }) + title!: string; + + @Prop({ default: '', trim: true, maxlength: 2000 }) + description!: string; + + @Prop({ required: true, min: 0 }) + price!: number; + + @Prop({ default: 'SAR', trim: true, maxlength: 8, uppercase: true }) + currency!: string; + + @Prop({ required: true, min: 0, default: 1 }) + quantity!: number; + + @Prop({ type: [String], default: [] }) + imageUrls!: string[]; + + @Prop({ default: true, index: true }) + isActive!: boolean; +} + +export const InstrumentSchema = SchemaFactory.createForClass(Instrument); +InstrumentSchema.index({ ownerAdminId: 1, createdAt: -1 }); +InstrumentSchema.index({ isActive: 1, createdAt: -1 }); +InstrumentSchema.index({ title: 1, isActive: 1, createdAt: -1 }); diff --git a/src/modules/media/dto/upload-media.dto.ts b/src/modules/media/dto/upload-media.dto.ts new file mode 100644 index 0000000..0a0a28c --- /dev/null +++ b/src/modules/media/dto/upload-media.dto.ts @@ -0,0 +1,7 @@ +import { IsOptional, IsString } from 'class-validator'; + +export class UploadMediaDto { + @IsOptional() + @IsString() + folder?: string; +} diff --git a/src/modules/media/media.controller.ts b/src/modules/media/media.controller.ts new file mode 100644 index 0000000..066a490 --- /dev/null +++ b/src/modules/media/media.controller.ts @@ -0,0 +1,6 @@ +import { Controller } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; + +@ApiTags('Media') +@Controller('media') +export class MediaController {} diff --git a/src/modules/media/media.module.ts b/src/modules/media/media.module.ts new file mode 100644 index 0000000..c24cde2 --- /dev/null +++ b/src/modules/media/media.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { MediaController } from './media.controller'; +import { MediaService } from './media.service'; +import { MediaRepository } from './media.repository'; + +@Module({ + controllers: [MediaController], + providers: [MediaService, MediaRepository], + exports: [MediaService], +}) +export class MediaModule {} diff --git a/src/modules/media/media.repository.ts b/src/modules/media/media.repository.ts new file mode 100644 index 0000000..36b1567 --- /dev/null +++ b/src/modules/media/media.repository.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class MediaRepository {} diff --git a/src/modules/media/media.service.ts b/src/modules/media/media.service.ts new file mode 100644 index 0000000..cb815d3 --- /dev/null +++ b/src/modules/media/media.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class MediaService {} diff --git a/src/modules/media/schemas/media-file.schema.ts b/src/modules/media/schemas/media-file.schema.ts new file mode 100644 index 0000000..4eb557a --- /dev/null +++ b/src/modules/media/schemas/media-file.schema.ts @@ -0,0 +1,22 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { HydratedDocument, Types } from 'mongoose'; + +export type MediaFileDocument = HydratedDocument; + +@Schema({ timestamps: true, versionKey: false }) +export class MediaFile { + @Prop({ type: Types.ObjectId, required: true, index: true }) + ownerId!: Types.ObjectId; + + @Prop({ required: true }) + url!: string; + + @Prop({ required: true }) + mimeType!: string; + + @Prop({ required: true }) + size!: number; +} + +export const MediaFileSchema = SchemaFactory.createForClass(MediaFile); +MediaFileSchema.index({ ownerId: 1, createdAt: -1 }); diff --git a/src/modules/notifications/dto/create-notification.dto.ts b/src/modules/notifications/dto/create-notification.dto.ts new file mode 100644 index 0000000..e5b4072 --- /dev/null +++ b/src/modules/notifications/dto/create-notification.dto.ts @@ -0,0 +1,16 @@ +import { IsEnum, IsMongoId, IsOptional } from 'class-validator'; + +export class CreateNotificationDto { + @IsMongoId() + recipientId!: string; + + @IsMongoId() + actorId!: string; + + @IsEnum(['like', 'comment', 'follow', 'message']) + type!: 'like' | 'comment' | 'follow' | 'message'; + + @IsOptional() + @IsMongoId() + referenceId?: string; +} diff --git a/src/modules/notifications/dto/notification-query.dto.ts b/src/modules/notifications/dto/notification-query.dto.ts new file mode 100644 index 0000000..c66070a --- /dev/null +++ b/src/modules/notifications/dto/notification-query.dto.ts @@ -0,0 +1,14 @@ +import { PaginationQueryDto } from '../../../common/dto/pagination-query.dto'; +import { Transform } from 'class-transformer'; +import { IsBoolean, IsOptional } from 'class-validator'; + +export class NotificationQueryDto extends PaginationQueryDto { + @IsOptional() + @Transform(({ value }) => { + if (value === 'true' || value === true) return true; + if (value === 'false' || value === false) return false; + return value; + }) + @IsBoolean() + read?: boolean; +} diff --git a/src/modules/notifications/notifications.controller.ts b/src/modules/notifications/notifications.controller.ts new file mode 100644 index 0000000..274bb3d --- /dev/null +++ b/src/modules/notifications/notifications.controller.ts @@ -0,0 +1,35 @@ +import { Controller, Get, Param, Patch, Query, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { CurrentUser } from '../../common/decorators/current-user.decorator'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { JwtPayload } from '../../common/interfaces/jwt-payload.interface'; +import { NotificationQueryDto } from './dto/notification-query.dto'; +import { NotificationsService } from './notifications.service'; + +@ApiTags('Notifications') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard) +@Controller('notifications') +export class NotificationsController { + constructor(private readonly notificationsService: NotificationsService) {} + + @Get() + async getMine(@CurrentUser() user: JwtPayload, @Query() query: NotificationQueryDto) { + return this.notificationsService.getMine(user.sub, query); + } + + @Get('unread-count') + async getUnreadCount(@CurrentUser() user: JwtPayload) { + return this.notificationsService.getUnreadCount(user.sub); + } + + @Patch('read-all') + async markAllRead(@CurrentUser() user: JwtPayload) { + return this.notificationsService.markAllRead(user.sub); + } + + @Patch(':id/read') + async markRead(@CurrentUser() user: JwtPayload, @Param('id') notificationId: string) { + return this.notificationsService.markRead(user.sub, notificationId); + } +} diff --git a/src/modules/notifications/notifications.gateway.ts b/src/modules/notifications/notifications.gateway.ts new file mode 100644 index 0000000..2ef66b7 --- /dev/null +++ b/src/modules/notifications/notifications.gateway.ts @@ -0,0 +1,71 @@ +import { ConfigService } from '@nestjs/config'; +import { JwtService } from '@nestjs/jwt'; +import { + OnGatewayConnection, + WebSocketGateway, + WebSocketServer, +} from '@nestjs/websockets'; +import { Server, Socket } from 'socket.io'; + +type SocketWithUser = Socket & { data: { userId?: string } }; + +@WebSocketGateway({ cors: { origin: '*' }, namespace: 'notifications' }) +export class NotificationsGateway implements OnGatewayConnection { + @WebSocketServer() + server!: Server; + + constructor( + private readonly jwtService: JwtService, + private readonly configService: ConfigService, + ) {} + + async handleConnection(client: SocketWithUser) { + const token = this.extractToken(client); + if (!token) { + client.disconnect(true); + return; + } + + try { + const payload = this.jwtService.verify<{ sub: string; tokenType: string }>(token, { + secret: this.configService.get('jwt.accessSecret', { infer: true }), + }); + if (payload.tokenType !== 'access') { + client.disconnect(true); + return; + } + + client.data.userId = payload.sub; + await client.join(this.userRoom(payload.sub)); + } catch { + client.disconnect(true); + } + } + + emitCreated(recipientId: string, notification: unknown, unreadCount: number): void { + this.server.to(this.userRoom(recipientId)).emit('notification_created', notification); + this.server.to(this.userRoom(recipientId)).emit('notifications_unread_count', { unreadCount }); + } + + emitUnreadCount(recipientId: string, unreadCount: number): void { + this.server.to(this.userRoom(recipientId)).emit('notifications_unread_count', { unreadCount }); + } + + private extractToken(client: Socket): string | null { + const authToken = client.handshake.auth?.token; + if (typeof authToken === 'string' && authToken.trim()) { + return authToken.replace(/^Bearer\s+/i, '').trim(); + } + + const headerAuth = client.handshake.headers.authorization; + if (typeof headerAuth === 'string' && headerAuth.trim()) { + return headerAuth.replace(/^Bearer\s+/i, '').trim(); + } + + return null; + } + + private userRoom(userId: string): string { + return `user:${userId}`; + } +} diff --git a/src/modules/notifications/notifications.module.ts b/src/modules/notifications/notifications.module.ts new file mode 100644 index 0000000..9a1cbfa --- /dev/null +++ b/src/modules/notifications/notifications.module.ts @@ -0,0 +1,21 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { JwtModule } from '@nestjs/jwt'; +import { MongooseModule } from '@nestjs/mongoose'; +import { NotificationsController } from './notifications.controller'; +import { NotificationsGateway } from './notifications.gateway'; +import { NotificationsService } from './notifications.service'; +import { NotificationsRepository } from './notifications.repository'; +import { Notification, NotificationSchema } from './schemas/notification.schema'; + +@Module({ + imports: [ + ConfigModule, + JwtModule.register({}), + MongooseModule.forFeature([{ name: Notification.name, schema: NotificationSchema }]), + ], + controllers: [NotificationsController], + providers: [NotificationsService, NotificationsRepository, NotificationsGateway], + exports: [NotificationsService], +}) +export class NotificationsModule {} diff --git a/src/modules/notifications/notifications.repository.ts b/src/modules/notifications/notifications.repository.ts new file mode 100644 index 0000000..e72a104 --- /dev/null +++ b/src/modules/notifications/notifications.repository.ts @@ -0,0 +1,100 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { FilterQuery, Model, Types } from 'mongoose'; +import { Notification, NotificationDocument } from './schemas/notification.schema'; + +@Injectable() +export class NotificationsRepository { + constructor( + @InjectModel(Notification.name) + private readonly notificationModel: Model, + ) {} + + async create(payload: Partial): Promise { + const notification = await this.notificationModel.create(payload); + const hydrated = await this.findById(notification.id); + if (!hydrated) { + throw new Error('Notification was created but could not be reloaded'); + } + return hydrated; + } + + async findById(id: string): Promise { + return this.notificationModel + .findById(id) + .populate({ path: 'actorId', select: 'name username stageName avatar isVerified isDisabled' }) + .exec(); + } + + async findMine( + recipientId: string, + filter: FilterQuery, + skip: number, + limit: number, + ): Promise { + return this.notificationModel + .find({ + recipientId: new Types.ObjectId(recipientId), + ...filter, + }) + .populate({ path: 'actorId', select: 'name username stageName avatar isVerified isDisabled' }) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit) + .exec(); + } + + async countMine(recipientId: string, filter: FilterQuery): Promise { + return this.notificationModel + .countDocuments({ + recipientId: new Types.ObjectId(recipientId), + ...filter, + }) + .exec(); + } + + async countUnread(recipientId: string): Promise { + return this.notificationModel + .countDocuments({ + recipientId: new Types.ObjectId(recipientId), + read: false, + }) + .exec(); + } + + async markRead(recipientId: string, notificationId: string): Promise { + const updated = await this.notificationModel + .findOneAndUpdate( + { + _id: new Types.ObjectId(notificationId), + recipientId: new Types.ObjectId(recipientId), + }, + { + read: true, + readAt: new Date(), + }, + { new: true }, + ) + .populate({ path: 'actorId', select: 'name username stageName avatar isVerified isDisabled' }) + .exec(); + + return updated; + } + + async markAllRead(recipientId: string): Promise { + const result = await this.notificationModel + .updateMany( + { + recipientId: new Types.ObjectId(recipientId), + read: false, + }, + { + read: true, + readAt: new Date(), + }, + ) + .exec(); + + return result.modifiedCount ?? 0; + } +} diff --git a/src/modules/notifications/notifications.service.spec.ts b/src/modules/notifications/notifications.service.spec.ts new file mode 100644 index 0000000..72951a3 --- /dev/null +++ b/src/modules/notifications/notifications.service.spec.ts @@ -0,0 +1,44 @@ +import { NotFoundException } from '@nestjs/common'; +import { NotificationsService } from './notifications.service'; + +describe('NotificationsService', () => { + it('recalculates unread count after markAllRead', async () => { + const notificationsRepository = { + markAllRead: jest.fn().mockResolvedValue(4), + countUnread: jest.fn().mockResolvedValue(2), + }; + const notificationsGateway = { + emitUnreadCount: jest.fn(), + }; + + const service = new NotificationsService( + notificationsRepository as any, + notificationsGateway as any, + ); + + await expect(service.markAllRead('user-1')).resolves.toEqual({ + message: 'All notifications marked as read', + updatedCount: 4, + unreadCount: 2, + }); + expect(notificationsGateway.emitUnreadCount).toHaveBeenCalledWith('user-1', 2); + }); + + it('throws not found for invalid notification id in markRead', async () => { + const notificationsRepository = { + markRead: jest.fn(), + countUnread: jest.fn(), + }; + const notificationsGateway = { + emitUnreadCount: jest.fn(), + }; + + const service = new NotificationsService( + notificationsRepository as any, + notificationsGateway as any, + ); + + await expect(service.markRead('user-1', 'invalid-id')).rejects.toBeInstanceOf(NotFoundException); + expect(notificationsRepository.markRead).not.toHaveBeenCalled(); + }); +}); diff --git a/src/modules/notifications/notifications.service.ts b/src/modules/notifications/notifications.service.ts new file mode 100644 index 0000000..ea482b9 --- /dev/null +++ b/src/modules/notifications/notifications.service.ts @@ -0,0 +1,107 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { Types } from 'mongoose'; +import { CreateNotificationDto } from './dto/create-notification.dto'; +import { NotificationQueryDto } from './dto/notification-query.dto'; +import { NotificationsGateway } from './notifications.gateway'; +import { NotificationsRepository } from './notifications.repository'; + +@Injectable() +export class NotificationsService { + constructor( + private readonly notificationsRepository: NotificationsRepository, + private readonly notificationsGateway: NotificationsGateway, + ) {} + + async create(dto: CreateNotificationDto) { + if (dto.recipientId === dto.actorId) { + return null; + } + + const notification = await this.notificationsRepository.create({ + recipientId: new Types.ObjectId(dto.recipientId), + actorId: new Types.ObjectId(dto.actorId), + type: dto.type, + referenceId: dto.referenceId ? new Types.ObjectId(dto.referenceId) : undefined, + read: false, + readAt: null, + }); + + const unreadCount = await this.notificationsRepository.countUnread(dto.recipientId); + this.notificationsGateway.emitCreated(dto.recipientId, notification.toJSON(), unreadCount); + + return notification; + } + + async createFollowNotification(actorId: string, recipientId: string, referenceId?: string) { + return this.create({ + actorId, + recipientId, + type: 'follow', + referenceId, + }); + } + + async getMine(recipientId: string, query: NotificationQueryDto) { + const page = query.page ?? 1; + const limit = query.limit ?? 20; + const skip = (page - 1) * limit; + const filter: Record = {}; + + if (typeof query.read === 'boolean') { + filter.read = query.read; + } + + const [items, total, unreadCount] = await Promise.all([ + this.notificationsRepository.findMine(recipientId, filter, skip, limit), + this.notificationsRepository.countMine(recipientId, filter), + this.notificationsRepository.countUnread(recipientId), + ]); + + return { + items, + unreadCount, + page, + limit, + total, + totalPages: Math.ceil(total / limit) || 1, + }; + } + + async getUnreadCount(recipientId: string) { + return { + unreadCount: await this.notificationsRepository.countUnread(recipientId), + }; + } + + async markRead(recipientId: string, notificationId: string) { + if (!Types.ObjectId.isValid(notificationId)) { + throw new NotFoundException('Notification not found'); + } + + const notification = await this.notificationsRepository.markRead(recipientId, notificationId); + if (!notification) { + throw new NotFoundException('Notification not found'); + } + + const unreadCount = await this.notificationsRepository.countUnread(recipientId); + this.notificationsGateway.emitUnreadCount(recipientId, unreadCount); + + return { + message: 'Notification marked as read', + unreadCount, + item: notification, + }; + } + + async markAllRead(recipientId: string) { + const updatedCount = await this.notificationsRepository.markAllRead(recipientId); + const unreadCount = await this.notificationsRepository.countUnread(recipientId); + this.notificationsGateway.emitUnreadCount(recipientId, unreadCount); + + return { + message: 'All notifications marked as read', + updatedCount, + unreadCount, + }; + } +} diff --git a/src/modules/notifications/schemas/notification.schema.ts b/src/modules/notifications/schemas/notification.schema.ts new file mode 100644 index 0000000..b0d49e0 --- /dev/null +++ b/src/modules/notifications/schemas/notification.schema.ts @@ -0,0 +1,31 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { HydratedDocument, Types } from 'mongoose'; +import { User } from '../../users/schemas/user.schema'; + +export type NotificationDocument = HydratedDocument; + +@Schema({ timestamps: true, versionKey: false }) +export class Notification { + @Prop({ type: Types.ObjectId, ref: User.name, required: true, index: true }) + recipientId!: Types.ObjectId; + + @Prop({ type: Types.ObjectId, ref: User.name, required: true, index: true }) + actorId!: Types.ObjectId; + + @Prop({ required: true, enum: ['like', 'comment', 'follow', 'message'] }) + type!: 'like' | 'comment' | 'follow' | 'message'; + + @Prop({ type: Types.ObjectId }) + referenceId?: Types.ObjectId; + + @Prop({ default: false }) + read!: boolean; + + @Prop({ type: Date, default: null }) + readAt?: Date | null; +} + +export const NotificationSchema = SchemaFactory.createForClass(Notification); +NotificationSchema.index({ recipientId: 1, createdAt: -1 }); +NotificationSchema.index({ recipientId: 1, read: 1, createdAt: -1 }); +NotificationSchema.index({ recipientId: 1, read: 1, type: 1, createdAt: -1 }); diff --git a/src/modules/outbox/outbox.module.ts b/src/modules/outbox/outbox.module.ts new file mode 100644 index 0000000..514890e --- /dev/null +++ b/src/modules/outbox/outbox.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { NotificationsModule } from '../notifications/notifications.module'; +import { OutboxService } from './outbox.service'; +import { OutboxEvent, OutboxEventSchema } from './schemas/outbox-event.schema'; + +@Module({ + imports: [ + MongooseModule.forFeature([{ name: OutboxEvent.name, schema: OutboxEventSchema }]), + NotificationsModule, + ], + providers: [OutboxService], + exports: [OutboxService], +}) +export class OutboxModule {} diff --git a/src/modules/outbox/outbox.service.ts b/src/modules/outbox/outbox.service.ts new file mode 100644 index 0000000..17537ba --- /dev/null +++ b/src/modules/outbox/outbox.service.ts @@ -0,0 +1,57 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { NotificationsService } from '../notifications/notifications.service'; +import { OutboxEvent, OutboxEventDocument } from './schemas/outbox-event.schema'; + +@Injectable() +export class OutboxService { + private readonly logger = new Logger(OutboxService.name); + + constructor( + @InjectModel(OutboxEvent.name) private readonly outboxEventModel: Model, + private readonly notificationsService: NotificationsService, + ) {} + + async enqueueFollowNotification(actorId: string, recipientId: string, referenceId?: string): Promise { + const event = await this.outboxEventModel.create({ + eventType: 'follow_notification', + payload: { + actorId, + recipientId, + referenceId: referenceId ?? '', + }, + status: 'pending', + }); + + await this.processEvent(event.id); + } + + async processEvent(eventId: string): Promise { + const event = await this.outboxEventModel.findById(eventId).exec(); + if (!event || event.status === 'processed') { + return; + } + + try { + if (event.eventType === 'follow_notification') { + await this.notificationsService.createFollowNotification( + String(event.payload.actorId ?? ''), + String(event.payload.recipientId ?? ''), + String(event.payload.referenceId ?? ''), + ); + } + + event.status = 'processed'; + event.processedAt = new Date(); + event.lastError = ''; + } catch (error) { + event.status = 'failed'; + event.lastError = error instanceof Error ? error.message : 'unknown outbox error'; + this.logger.warn(`Outbox event ${event.id} failed: ${event.lastError}`); + } finally { + event.attempts += 1; + await event.save(); + } + } +} diff --git a/src/modules/outbox/schemas/outbox-event.schema.ts b/src/modules/outbox/schemas/outbox-event.schema.ts new file mode 100644 index 0000000..03814c6 --- /dev/null +++ b/src/modules/outbox/schemas/outbox-event.schema.ts @@ -0,0 +1,28 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { HydratedDocument } from 'mongoose'; + +export type OutboxEventDocument = HydratedDocument; + +@Schema({ timestamps: true, versionKey: false }) +export class OutboxEvent { + @Prop({ required: true, index: true }) + eventType!: string; + + @Prop({ required: true, type: Object }) + payload!: Record; + + @Prop({ required: true, default: 'pending', enum: ['pending', 'processed', 'failed'], index: true }) + status!: 'pending' | 'processed' | 'failed'; + + @Prop({ required: true, default: 0, min: 0 }) + attempts!: number; + + @Prop({ default: '' }) + lastError!: string; + + @Prop({ type: Date, default: null, index: true }) + processedAt?: Date | null; +} + +export const OutboxEventSchema = SchemaFactory.createForClass(OutboxEvent); +OutboxEventSchema.index({ status: 1, createdAt: 1 }); diff --git a/src/modules/posts/dto/create-post.dto.ts b/src/modules/posts/dto/create-post.dto.ts new file mode 100644 index 0000000..2a63780 --- /dev/null +++ b/src/modules/posts/dto/create-post.dto.ts @@ -0,0 +1,25 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsEnum, IsOptional, IsString, IsUrl, Length } from 'class-validator'; +import { PostVisibility } from '../../../common/enums/post-visibility.enum'; + +export class CreatePostDto { + @ApiProperty({ maxLength: 2200, description: 'Post description/content' }) + @IsString() + @Length(1, 2200) + content!: string; + + @ApiPropertyOptional({ description: 'Single video URL (optional)' }) + @IsOptional() + @IsUrl({ require_tld: false }) + videoUrl?: string; + + @ApiPropertyOptional({ description: 'Single audio URL (optional)' }) + @IsOptional() + @IsUrl({ require_tld: false }) + audioUrl?: string; + + @ApiPropertyOptional({ enum: PostVisibility, default: PostVisibility.PUBLIC }) + @IsOptional() + @IsEnum(PostVisibility) + visibility?: PostVisibility; +} diff --git a/src/modules/posts/dto/post-query.dto.ts b/src/modules/posts/dto/post-query.dto.ts new file mode 100644 index 0000000..9d4f3b6 --- /dev/null +++ b/src/modules/posts/dto/post-query.dto.ts @@ -0,0 +1,11 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsEnum, IsOptional } from 'class-validator'; +import { PaginationQueryDto } from '../../../common/dto/pagination-query.dto'; +import { PostVisibility } from '../../../common/enums/post-visibility.enum'; + +export class PostQueryDto extends PaginationQueryDto { + @ApiPropertyOptional({ enum: PostVisibility }) + @IsOptional() + @IsEnum(PostVisibility) + visibility?: PostVisibility; +} diff --git a/src/modules/posts/dto/update-post.dto.ts b/src/modules/posts/dto/update-post.dto.ts new file mode 100644 index 0000000..a1c0958 --- /dev/null +++ b/src/modules/posts/dto/update-post.dto.ts @@ -0,0 +1,26 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsEnum, IsOptional, IsString, IsUrl, Length } from 'class-validator'; +import { PostVisibility } from '../../../common/enums/post-visibility.enum'; + +export class UpdatePostDto { + @ApiPropertyOptional({ maxLength: 2200 }) + @IsOptional() + @IsString() + @Length(1, 2200) + content?: string; + + @ApiPropertyOptional({ description: 'Set video URL. If provided, audioUrl will be cleared.' }) + @IsOptional() + @IsUrl({ require_tld: false }) + videoUrl?: string; + + @ApiPropertyOptional({ description: 'Set audio URL. If provided, videoUrl will be cleared.' }) + @IsOptional() + @IsUrl({ require_tld: false }) + audioUrl?: string; + + @ApiPropertyOptional({ enum: PostVisibility }) + @IsOptional() + @IsEnum(PostVisibility) + visibility?: PostVisibility; +} diff --git a/src/modules/posts/posts.controller.ts b/src/modules/posts/posts.controller.ts new file mode 100644 index 0000000..cf93164 --- /dev/null +++ b/src/modules/posts/posts.controller.ts @@ -0,0 +1,55 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { CurrentUser } from '../../common/decorators/current-user.decorator'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { JwtPayload } from '../../common/interfaces/jwt-payload.interface'; +import { CreatePostDto } from './dto/create-post.dto'; +import { PostQueryDto } from './dto/post-query.dto'; +import { UpdatePostDto } from './dto/update-post.dto'; +import { PostsService } from './posts.service'; + +@ApiTags('Posts') +@Controller('posts') +export class PostsController { + constructor(private readonly postsService: PostsService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post() + async create(@CurrentUser() user: JwtPayload, @Body() dto: CreatePostDto) { + return this.postsService.create(user.sub, dto); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get('user/:userId') + async findUserPosts(@Param('userId') userId: string, @Query() query: PostQueryDto) { + return this.postsService.findUserPosts(userId, query); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':postId') + async findById(@Param('postId') postId: string) { + return this.postsService.findById(postId); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Patch(':postId') + async update( + @CurrentUser() user: JwtPayload, + @Param('postId') postId: string, + @Body() dto: UpdatePostDto, + ) { + return this.postsService.update(user.sub, postId, dto); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Delete(':postId') + async remove(@CurrentUser() user: JwtPayload, @Param('postId') postId: string) { + await this.postsService.remove(user.sub, postId); + return { success: true }; + } +} diff --git a/src/modules/posts/posts.module.ts b/src/modules/posts/posts.module.ts new file mode 100644 index 0000000..2a537fb --- /dev/null +++ b/src/modules/posts/posts.module.ts @@ -0,0 +1,23 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { UsersModule } from '../users/users.module'; +import { Post, PostSchema } from './schemas/post.schema'; +import { PostsController } from './posts.controller'; +import { PostsRepository } from './posts.repository'; +import { PostsService } from './posts.service'; + +@Module({ + imports: [ + MongooseModule.forFeature([ + { + name: Post.name, + schema: PostSchema, + }, + ]), + UsersModule, + ], + controllers: [PostsController], + providers: [PostsService, PostsRepository], + exports: [PostsService, PostsRepository], +}) +export class PostsModule {} diff --git a/src/modules/posts/posts.repository.ts b/src/modules/posts/posts.repository.ts new file mode 100644 index 0000000..ec5c57d --- /dev/null +++ b/src/modules/posts/posts.repository.ts @@ -0,0 +1,121 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { ClientSession, FilterQuery, Model, Types, UpdateQuery } from 'mongoose'; +import { Post, PostDocument } from './schemas/post.schema'; + +@Injectable() +export class PostsRepository { + constructor(@InjectModel(Post.name) private readonly postModel: Model) {} + + private withActiveFilter>(filter: T): FilterQuery { + return { + ...filter, + isDeleted: { $ne: true }, + }; + } + + async create(authorId: string, payload: Partial): Promise { + return this.postModel.create({ + ...payload, + authorId: new Types.ObjectId(authorId), + }); + } + + async findById(postId: string): Promise { + if (!Types.ObjectId.isValid(postId)) { + return null; + } + + return this.postModel + .findOne({ _id: new Types.ObjectId(postId), isDeleted: { $ne: true } }) + .exec(); + } + + async updateById(postId: string, payload: UpdateQuery): Promise { + if (!Types.ObjectId.isValid(postId)) { + return null; + } + + return this.postModel.findByIdAndUpdate(postId, payload, { new: true }).exec(); + } + + async deleteById(postId: string, deletedBy?: string): Promise { + if (!Types.ObjectId.isValid(postId)) { + return null; + } + + const deletedByObjectId = + deletedBy && Types.ObjectId.isValid(deletedBy) ? new Types.ObjectId(deletedBy) : null; + + return this.postModel + .findOneAndUpdate( + { _id: new Types.ObjectId(postId), isDeleted: { $ne: true } }, + { isDeleted: true, deletedAt: new Date(), deletedBy: deletedByObjectId }, + { new: true }, + ) + .exec(); + } + + async findMany(filter: FilterQuery, skip: number, limit: number): Promise { + return this.postModel + .find(this.withActiveFilter(filter)) + .populate({ path: 'authorId', select: 'name username avatar isVerified stageName' }) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit) + .exec(); + } + + async findManyByIds(postIds: string[]): Promise { + if (!postIds.length) { + return []; + } + + const ids = postIds.map((id) => new Types.ObjectId(id)); + const rows = await this.postModel + .find({ _id: { $in: ids }, isDeleted: { $ne: true } }) + .populate({ path: 'authorId', select: 'name username avatar isVerified stageName' }) + .exec(); + + const order = new Map(postIds.map((id, idx) => [id, idx])); + return rows.sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)); + } + + async incrementLikesCount(postId: string, delta: 1 | -1, session?: ClientSession): Promise { + await this.postModel + .findByIdAndUpdate(postId, { $inc: { likesCount: delta } }, { new: false, session }) + .exec(); + } + + async incrementCommentsCount( + postId: string, + delta: 1 | -1, + session?: ClientSession, + ): Promise { + await this.postModel + .findByIdAndUpdate(postId, { $inc: { commentsCount: delta } }, { new: false, session }) + .exec(); + } + + async incrementSavesCount(postId: string, delta: 1 | -1, session?: ClientSession): Promise { + await this.postModel + .findByIdAndUpdate(postId, { $inc: { savesCount: delta } }, { new: false, session }) + .exec(); + } + + async setCommentsCount(postId: string, nextValue: number, session?: ClientSession): Promise { + await this.postModel + .findByIdAndUpdate( + postId, + { + commentsCount: Math.max(0, nextValue), + }, + { new: false, session }, + ) + .exec(); + } + + async count(filter: FilterQuery): Promise { + return this.postModel.countDocuments(this.withActiveFilter(filter)).exec(); + } +} diff --git a/src/modules/posts/posts.service.ts b/src/modules/posts/posts.service.ts new file mode 100644 index 0000000..db33090 --- /dev/null +++ b/src/modules/posts/posts.service.ts @@ -0,0 +1,155 @@ +import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common'; +import { Types } from 'mongoose'; +import { PostType } from '../../common/enums/post-type.enum'; +import { PostVisibility } from '../../common/enums/post-visibility.enum'; +import { UsersRepository } from '../users/users.repository'; +import { CreatePostDto } from './dto/create-post.dto'; +import { PostQueryDto } from './dto/post-query.dto'; +import { UpdatePostDto } from './dto/update-post.dto'; +import { PostDocument } from './schemas/post.schema'; +import { PostsRepository } from './posts.repository'; + +@Injectable() +export class PostsService { + constructor( + private readonly postsRepository: PostsRepository, + private readonly usersRepository: UsersRepository, + ) {} + + async create(userId: string, dto: CreatePostDto): Promise { + const postType = this.resolvePostType(dto.videoUrl, dto.audioUrl); + const hashtags = this.extractHashtags(dto.content); + + const post = await this.postsRepository.create(userId, { + content: dto.content, + videoUrl: dto.videoUrl ?? '', + audioUrl: dto.audioUrl ?? '', + postType, + visibility: dto.visibility ?? PostVisibility.PUBLIC, + hashtags, + }); + + await this.usersRepository.incrementPostsCount(userId, 1); + return post; + } + + async update(userId: string, postId: string, dto: UpdatePostDto): Promise { + const post = await this.postsRepository.findById(postId); + if (!post) { + throw new NotFoundException('Post not found'); + } + + if (post.authorId.toString() !== userId) { + throw new ForbiddenException('You can only update your own posts'); + } + + const hasVideoUpdate = typeof dto.videoUrl !== 'undefined'; + const hasAudioUpdate = typeof dto.audioUrl !== 'undefined'; + + const nextVideoUrl = hasVideoUpdate ? dto.videoUrl ?? '' : post.videoUrl ?? ''; + const nextAudioUrl = hasAudioUpdate ? dto.audioUrl ?? '' : post.audioUrl ?? ''; + const nextPostType = this.resolvePostType(nextVideoUrl, nextAudioUrl); + + const payload: UpdatePostDto & { postType: PostType } = { + ...dto, + postType: nextPostType, + }; + + if (typeof dto.content === 'string') { + (payload as UpdatePostDto & { postType: PostType; hashtags: string[] }).hashtags = + this.extractHashtags(dto.content); + } + + if (hasVideoUpdate && !hasAudioUpdate) { + payload.audioUrl = ''; + } + if (hasAudioUpdate && !hasVideoUpdate) { + payload.videoUrl = ''; + } + + const updated = await this.postsRepository.updateById(postId, payload); + if (!updated) { + throw new NotFoundException('Post not found'); + } + + return updated; + } + + async remove(userId: string, postId: string): Promise { + const post = await this.postsRepository.findById(postId); + if (!post) { + throw new NotFoundException('Post not found'); + } + + if (post.authorId.toString() !== userId) { + throw new ForbiddenException('You can only delete your own posts'); + } + + await this.postsRepository.deleteById(postId, userId); + await this.usersRepository.incrementPostsCount(userId, -1); + } + + async findById(postId: string): Promise { + const post = await this.postsRepository.findById(postId); + if (!post) { + throw new NotFoundException('Post not found'); + } + return post; + } + + async findUserPosts(userId: string, query: PostQueryDto) { + if (!Types.ObjectId.isValid(userId)) { + throw new BadRequestException('Invalid user id'); + } + + const page = query.page ?? 1; + const limit = query.limit ?? 20; + const skip = (page - 1) * limit; + + const filter: Record = { authorId: new Types.ObjectId(userId) }; + if (query.visibility) { + filter.visibility = query.visibility; + } + + const [items, total] = await Promise.all([ + this.postsRepository.findMany(filter, skip, limit), + this.postsRepository.count(filter), + ]); + + return { + items, + page, + limit, + total, + totalPages: Math.ceil(total / limit) || 1, + }; + } + + private resolvePostType(videoUrl?: string, audioUrl?: string): PostType { + const hasVideo = !!videoUrl?.trim(); + const hasAudio = !!audioUrl?.trim(); + + if (hasVideo && hasAudio) { + throw new BadRequestException('Post can contain either video or audio, not both'); + } + + if (hasVideo) { + return PostType.VIDEO; + } + + if (hasAudio) { + return PostType.AUDIO; + } + + return PostType.TEXT; + } + + private extractHashtags(content: string): string[] { + const matches = content.match(/#[\p{L}\p{N}_]+/gu) ?? []; + const normalized = matches + .map((item) => item.replace('#', '').trim().toLowerCase()) + .filter(Boolean); + + return Array.from(new Set(normalized)).slice(0, 20); + } +} diff --git a/src/modules/posts/schemas/post.schema.ts b/src/modules/posts/schemas/post.schema.ts new file mode 100644 index 0000000..3994a30 --- /dev/null +++ b/src/modules/posts/schemas/post.schema.ts @@ -0,0 +1,59 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { HydratedDocument, Types } from 'mongoose'; +import { PostType } from '../../../common/enums/post-type.enum'; +import { PostVisibility } from '../../../common/enums/post-visibility.enum'; +import { User } from '../../users/schemas/user.schema'; + +export type PostDocument = HydratedDocument; + +@Schema({ timestamps: true, versionKey: false }) +export class Post { + @Prop({ type: Types.ObjectId, ref: User.name, required: true, index: true }) + authorId!: Types.ObjectId; + + @Prop({ default: '', trim: true, maxlength: 2200, required: true }) + content!: string; + + @Prop({ default: '' }) + videoUrl!: string; + + @Prop({ default: '' }) + audioUrl!: string; + + @Prop({ enum: PostType, default: PostType.TEXT, index: true }) + postType!: PostType; + + @Prop({ enum: PostVisibility, default: PostVisibility.PUBLIC, index: true }) + visibility!: PostVisibility; + + @Prop({ default: 0, min: 0 }) + likesCount!: number; + + @Prop({ default: 0, min: 0 }) + commentsCount!: number; + + @Prop({ default: 0, min: 0 }) + savesCount!: number; + + @Prop({ type: [String], default: [], index: true }) + hashtags!: string[]; + + @Prop({ default: false, index: true }) + isDeleted!: boolean; + + @Prop({ type: Date, default: null }) + deletedAt?: Date | null; + + @Prop({ type: Types.ObjectId, ref: User.name, default: null }) + deletedBy?: Types.ObjectId | null; +} + +export const PostSchema = SchemaFactory.createForClass(Post); + +PostSchema.index({ authorId: 1, createdAt: -1 }); +PostSchema.index({ visibility: 1, createdAt: -1 }); +PostSchema.index({ postType: 1, createdAt: -1 }); +PostSchema.index({ hashtags: 1, createdAt: -1 }); +PostSchema.index({ authorId: 1, isDeleted: 1, createdAt: -1 }); +PostSchema.index({ visibility: 1, isDeleted: 1, createdAt: -1 }); +PostSchema.index({ visibility: 1, isDeleted: 1, likesCount: -1, commentsCount: -1, savesCount: -1, createdAt: -1 }); diff --git a/src/modules/saves/dto/toggle-save.dto.ts b/src/modules/saves/dto/toggle-save.dto.ts new file mode 100644 index 0000000..8b0c875 --- /dev/null +++ b/src/modules/saves/dto/toggle-save.dto.ts @@ -0,0 +1,6 @@ +import { IsMongoId } from 'class-validator'; + +export class ToggleSaveDto { + @IsMongoId() + postId!: string; +} diff --git a/src/modules/saves/saves.controller.ts b/src/modules/saves/saves.controller.ts new file mode 100644 index 0000000..6107f71 --- /dev/null +++ b/src/modules/saves/saves.controller.ts @@ -0,0 +1,41 @@ +import { Body, Controller, Delete, Get, Param, Post, Query, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { CurrentUser } from '../../common/decorators/current-user.decorator'; +import { PaginationQueryDto } from '../../common/dto/pagination-query.dto'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { JwtPayload } from '../../common/interfaces/jwt-payload.interface'; +import { ToggleSaveDto } from './dto/toggle-save.dto'; +import { SavesService } from './saves.service'; + +@ApiTags('Saves') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard) +@Controller('saves') +export class SavesController { + constructor(private readonly savesService: SavesService) {} + + @Post() + async save(@CurrentUser() user: JwtPayload, @Body() dto: ToggleSaveDto) { + return this.savesService.save(user.sub, dto); + } + + @Delete(':postId') + async unsave(@CurrentUser() user: JwtPayload, @Param('postId') postId: string) { + return this.savesService.unsave(user.sub, { postId }); + } + + @Get('status/:postId') + async getStatus(@CurrentUser() user: JwtPayload, @Param('postId') postId: string) { + return this.savesService.getStatus(user.sub, { postId }); + } + + @Post('toggle') + async toggle(@CurrentUser() user: JwtPayload, @Body() dto: ToggleSaveDto) { + return this.savesService.toggle(user.sub, dto); + } + + @Get('me') + async getMySavedPosts(@CurrentUser() user: JwtPayload, @Query() query: PaginationQueryDto) { + return this.savesService.getMySavedPosts(user.sub, query); + } +} diff --git a/src/modules/saves/saves.module.ts b/src/modules/saves/saves.module.ts new file mode 100644 index 0000000..d6d781e --- /dev/null +++ b/src/modules/saves/saves.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { PostsModule } from '../posts/posts.module'; +import { Save, SaveSchema } from './schemas/save.schema'; +import { SavesController } from './saves.controller'; +import { SavesRepository } from './saves.repository'; +import { SavesService } from './saves.service'; + +@Module({ + imports: [ + MongooseModule.forFeature([{ name: Save.name, schema: SaveSchema }]), + PostsModule, + ], + controllers: [SavesController], + providers: [SavesService, SavesRepository], + exports: [SavesService], +}) +export class SavesModule {} diff --git a/src/modules/saves/saves.repository.ts b/src/modules/saves/saves.repository.ts new file mode 100644 index 0000000..96372fe --- /dev/null +++ b/src/modules/saves/saves.repository.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model, Types } from 'mongoose'; +import { Save, SaveDocument } from './schemas/save.schema'; + +@Injectable() +export class SavesRepository { + constructor(@InjectModel(Save.name) private readonly saveModel: Model) {} + + async findOne(userId: string, postId: string): Promise { + return this.saveModel + .findOne({ userId: new Types.ObjectId(userId), postId: new Types.ObjectId(postId) }) + .exec(); + } + + async create(userId: string, postId: string): Promise { + return this.saveModel.create({ + userId: new Types.ObjectId(userId), + postId: new Types.ObjectId(postId), + }); + } + + async deleteById(id: string): Promise { + await this.saveModel.findByIdAndDelete(id).exec(); + } + + async findUserSavedPostIds(userId: string, skip: number, limit: number): Promise { + const rows = await this.saveModel + .find({ userId: new Types.ObjectId(userId) }) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(limit) + .select('postId') + .exec(); + + return rows.map((row) => row.postId.toString()); + } + + async countByUser(userId: string): Promise { + return this.saveModel.countDocuments({ userId: new Types.ObjectId(userId) }).exec(); + } +} diff --git a/src/modules/saves/saves.service.spec.ts b/src/modules/saves/saves.service.spec.ts new file mode 100644 index 0000000..b02e79f --- /dev/null +++ b/src/modules/saves/saves.service.spec.ts @@ -0,0 +1,25 @@ +import { SavesService } from './saves.service'; + +describe('SavesService', () => { + it('returns saved false from status when post no longer exists', async () => { + const savesRepository = { + findOne: jest.fn(), + }; + const postsRepository = { + findById: jest.fn().mockResolvedValue(null), + }; + + const service = new SavesService( + savesRepository as any, + postsRepository as any, + ); + + await expect( + service.getStatus('user-1', { postId: '507f1f77bcf86cd799439011' }), + ).resolves.toEqual({ + saved: false, + postId: '507f1f77bcf86cd799439011', + }); + expect(savesRepository.findOne).not.toHaveBeenCalled(); + }); +}); diff --git a/src/modules/saves/saves.service.ts b/src/modules/saves/saves.service.ts new file mode 100644 index 0000000..f694513 --- /dev/null +++ b/src/modules/saves/saves.service.ts @@ -0,0 +1,87 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PaginationQueryDto } from '../../common/dto/pagination-query.dto'; +import { PostsRepository } from '../posts/posts.repository'; +import { ToggleSaveDto } from './dto/toggle-save.dto'; +import { SavesRepository } from './saves.repository'; + +@Injectable() +export class SavesService { + constructor( + private readonly savesRepository: SavesRepository, + private readonly postsRepository: PostsRepository, + ) {} + + async toggle(userId: string, dto: ToggleSaveDto): Promise<{ saved: boolean; postId: string }> { + const existing = await this.savesRepository.findOne(userId, dto.postId); + return existing ? this.unsave(userId, dto) : this.save(userId, dto); + } + + async save(userId: string, dto: ToggleSaveDto): Promise<{ saved: boolean; postId: string }> { + await this.assertPostExists(dto.postId); + + const existing = await this.savesRepository.findOne(userId, dto.postId); + if (existing) { + return { saved: true, postId: dto.postId }; + } + + await this.savesRepository.create(userId, dto.postId); + await this.postsRepository.incrementSavesCount(dto.postId, 1); + return { saved: true, postId: dto.postId }; + } + + async unsave(userId: string, dto: ToggleSaveDto): Promise<{ saved: boolean; postId: string }> { + await this.assertPostExists(dto.postId); + + const existing = await this.savesRepository.findOne(userId, dto.postId); + if (!existing) { + return { saved: false, postId: dto.postId }; + } + + await this.savesRepository.deleteById(existing.id); + await this.postsRepository.incrementSavesCount(dto.postId, -1); + return { saved: false, postId: dto.postId }; + } + + async getStatus(userId: string, dto: ToggleSaveDto): Promise<{ saved: boolean; postId: string }> { + const postExists = await this.postExists(dto.postId); + if (!postExists) { + return { saved: false, postId: dto.postId }; + } + + const existing = await this.savesRepository.findOne(userId, dto.postId); + return { saved: !!existing, postId: dto.postId }; + } + + async getMySavedPosts(userId: string, query: PaginationQueryDto) { + const page = query.page ?? 1; + const limit = query.limit ?? 20; + const skip = (page - 1) * limit; + + const [postIds, total] = await Promise.all([ + this.savesRepository.findUserSavedPostIds(userId, skip, limit), + this.savesRepository.countByUser(userId), + ]); + + const items = await this.postsRepository.findManyByIds(postIds); + + return { + items, + page, + limit, + total, + totalPages: Math.ceil(total / limit) || 1, + }; + } + + private async assertPostExists(postId: string): Promise { + const exists = await this.postExists(postId); + if (!exists) { + throw new NotFoundException('Post not found'); + } + } + + private async postExists(postId: string): Promise { + const post = await this.postsRepository.findById(postId); + return !!post; + } +} diff --git a/src/modules/saves/schemas/save.schema.ts b/src/modules/saves/schemas/save.schema.ts new file mode 100644 index 0000000..e8021d2 --- /dev/null +++ b/src/modules/saves/schemas/save.schema.ts @@ -0,0 +1,17 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { HydratedDocument, Types } from 'mongoose'; + +export type SaveDocument = HydratedDocument; + +@Schema({ timestamps: true, versionKey: false }) +export class Save { + @Prop({ type: Types.ObjectId, required: true, index: true }) + userId!: Types.ObjectId; + + @Prop({ type: Types.ObjectId, required: true, index: true }) + postId!: Types.ObjectId; +} + +export const SaveSchema = SchemaFactory.createForClass(Save); +SaveSchema.index({ userId: 1, postId: 1 }, { unique: true }); +SaveSchema.index({ userId: 1, createdAt: -1 }); diff --git a/src/modules/users/dto/admin-disable-user.dto.ts b/src/modules/users/dto/admin-disable-user.dto.ts new file mode 100644 index 0000000..f1212d5 --- /dev/null +++ b/src/modules/users/dto/admin-disable-user.dto.ts @@ -0,0 +1,10 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsOptional, IsString, Length } from 'class-validator'; + +export class AdminDisableUserDto { + @ApiPropertyOptional({ example: 'Violation of community guidelines' }) + @IsOptional() + @IsString() + @Length(0, 300) + reason?: string; +} diff --git a/src/modules/users/dto/create-admin.dto.ts b/src/modules/users/dto/create-admin.dto.ts new file mode 100644 index 0000000..21892f6 --- /dev/null +++ b/src/modules/users/dto/create-admin.dto.ts @@ -0,0 +1,27 @@ +import { IsEmail, IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator'; + +export class CreateAdminDto { + @IsString() + @IsNotEmpty() + @MaxLength(80) + name!: string; + + @IsString() + @IsNotEmpty() + @MinLength(3) + @MaxLength(30) + username!: string; + + @IsEmail() + email!: string; + + @IsString() + @MinLength(8) + @MaxLength(128) + password!: string; + + @IsString() + @MinLength(8) + @MaxLength(128) + confirmPassword!: string; +} diff --git a/src/modules/users/dto/create-user.dto.ts b/src/modules/users/dto/create-user.dto.ts new file mode 100644 index 0000000..b84bd5e --- /dev/null +++ b/src/modules/users/dto/create-user.dto.ts @@ -0,0 +1,115 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsArray, + IsBoolean, + IsEmail, + IsEnum, + IsNumber, + IsOptional, + IsString, + IsUrl, + Length, + Max, + Matches, + Min, +} from 'class-validator'; +import { ExperienceLevel } from '../../../common/enums/experience-level.enum'; +import { MusicRole } from '../../../common/enums/music-role.enum'; + +export class CreateUserDto { + @ApiProperty({ example: 'John Doe' }) + @IsString() + @Length(2, 80) + name!: string; + + @ApiProperty({ required: false, example: 'Artist One' }) + @IsOptional() + @IsString() + @Length(0, 80) + stageName?: string; + + @ApiProperty({ example: 'john_doe' }) + @IsString() + @Length(3, 30) + @Matches(/^[a-zA-Z0-9_.]+$/, { message: 'username can contain letters, numbers, _ and .' }) + username!: string; + + @ApiProperty({ example: 'john@example.com' }) + @IsEmail() + email!: string; + + @ApiProperty({ minLength: 8, example: 'StrongPass123!' }) + @IsString() + @Length(8, 64) + password!: string; + + @ApiProperty({ required: false, maxLength: 160 }) + @IsOptional() + @IsString() + @Length(0, 160) + bio?: string; + + @ApiProperty({ required: false, example: 'https://cdn.example.com/avatar.jpg' }) + @IsOptional() + @IsUrl({ require_tld: false }) + avatar?: string; + + @ApiProperty({ required: false, example: 'Riyadh, Saudi Arabia' }) + @IsOptional() + @IsString() + @Length(0, 120) + location?: string; + + @ApiProperty({ required: false, example: 24.7136, minimum: -90, maximum: 90 }) + @IsOptional() + @IsNumber() + @Min(-90) + @Max(90) + latitude?: number; + + @ApiProperty({ required: false, example: 46.6753, minimum: -180, maximum: 180 }) + @IsOptional() + @IsNumber() + @Min(-180) + @Max(180) + longitude?: number; + + @ApiProperty({ required: false, default: false }) + @IsOptional() + @IsBoolean() + isPrivate?: boolean; + + @ApiProperty({ required: false, default: false }) + @IsOptional() + @IsBoolean() + isVerified?: boolean; + + @ApiProperty({ required: false, enum: MusicRole, isArray: true }) + @IsOptional() + @IsArray() + @IsEnum(MusicRole, { each: true }) + musicRoles?: MusicRole[]; + + @ApiProperty({ required: false, enum: ExperienceLevel, example: ExperienceLevel.BEGINNER }) + @IsOptional() + @IsEnum(ExperienceLevel) + experienceLevel?: ExperienceLevel; + + @ApiProperty({ required: false, type: [String], example: ['Tarab', 'Pop'] }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + musicGenres?: string[]; + + @ApiProperty({ required: false, type: [String], example: ['Oud', 'Piano'] }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + favoriteInstruments?: string[]; + + @ApiProperty({ required: false, type: [String], example: ['Bayati', 'Rast'] }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + favoriteMaqamat?: string[]; +} diff --git a/src/modules/users/dto/music-setup.dto.ts b/src/modules/users/dto/music-setup.dto.ts new file mode 100644 index 0000000..b589f99 --- /dev/null +++ b/src/modules/users/dto/music-setup.dto.ts @@ -0,0 +1,35 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsArray, IsEnum, IsOptional, IsString } from 'class-validator'; +import { ExperienceLevel } from '../../../common/enums/experience-level.enum'; +import { MusicRole } from '../../../common/enums/music-role.enum'; + +export class MusicSetupDto { + @ApiPropertyOptional({ enum: MusicRole, isArray: true }) + @IsOptional() + @IsArray() + @IsEnum(MusicRole, { each: true }) + musicRoles?: MusicRole[]; + + @ApiPropertyOptional({ type: [String], example: ['Tarab', 'Pop'] }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + musicGenres?: string[]; + + @ApiPropertyOptional({ enum: ExperienceLevel, example: ExperienceLevel.BEGINNER }) + @IsOptional() + @IsEnum(ExperienceLevel) + experienceLevel?: ExperienceLevel; + + @ApiPropertyOptional({ type: [String], example: ['Oud', 'Piano'] }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + favoriteInstruments?: string[]; + + @ApiPropertyOptional({ type: [String], example: ['Bayati', 'Rast'] }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + favoriteMaqamat?: string[]; +} diff --git a/src/modules/users/dto/profile-setup.dto.ts b/src/modules/users/dto/profile-setup.dto.ts new file mode 100644 index 0000000..24d60af --- /dev/null +++ b/src/modules/users/dto/profile-setup.dto.ts @@ -0,0 +1,44 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Transform, Type } from 'class-transformer'; +import { IsNumber, IsOptional, IsString, IsUrl, Length, Max, Min } from 'class-validator'; + +export class ProfileSetupDto { + @ApiPropertyOptional({ example: 'Artist One' }) + @IsOptional() + @IsString() + @Length(1, 80) + stageName?: string; + + @ApiPropertyOptional({ example: 'https://cdn.example.com/avatar.jpg' }) + @IsOptional() + @IsUrl({ require_tld: false }) + avatar?: string; + + @ApiPropertyOptional({ example: 'نبذة قصيرة عني', maxLength: 150 }) + @IsOptional() + @IsString() + @Length(0, 150) + bio?: string; + + @ApiPropertyOptional({ example: 'Riyadh, Saudi Arabia' }) + @IsOptional() + @IsString() + @Length(0, 120) + location?: string; + + @ApiProperty({ example: 24.7136, minimum: -90, maximum: 90 }) + @Transform(({ value }) => (typeof value === 'string' ? Number.parseFloat(value) : value)) + @Type(() => Number) + @IsNumber() + @Min(-90) + @Max(90) + latitude!: number; + + @ApiProperty({ example: 46.6753, minimum: -180, maximum: 180 }) + @Transform(({ value }) => (typeof value === 'string' ? Number.parseFloat(value) : value)) + @Type(() => Number) + @IsNumber() + @Min(-180) + @Max(180) + longitude!: number; +} diff --git a/src/modules/users/dto/update-user-role.dto.ts b/src/modules/users/dto/update-user-role.dto.ts new file mode 100644 index 0000000..05ef143 --- /dev/null +++ b/src/modules/users/dto/update-user-role.dto.ts @@ -0,0 +1,7 @@ +import { IsEnum } from 'class-validator'; +import { UserRole } from '../../../common/enums/user-role.enum'; + +export class UpdateUserRoleDto { + @IsEnum(UserRole) + role!: UserRole; +} diff --git a/src/modules/users/dto/update-user.dto.ts b/src/modules/users/dto/update-user.dto.ts new file mode 100644 index 0000000..d623311 --- /dev/null +++ b/src/modules/users/dto/update-user.dto.ts @@ -0,0 +1,69 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsArray, IsBoolean, IsEnum, IsOptional, IsString, IsUrl, Length } from 'class-validator'; +import { ExperienceLevel } from '../../../common/enums/experience-level.enum'; +import { MusicRole } from '../../../common/enums/music-role.enum'; + +export class UpdateUserDto { + @ApiPropertyOptional({ example: 'John Doe' }) + @IsOptional() + @IsString() + @Length(2, 80) + name?: string; + + @ApiPropertyOptional({ example: 'Artist One' }) + @IsOptional() + @IsString() + @Length(0, 80) + stageName?: string; + + @ApiPropertyOptional({ maxLength: 160 }) + @IsOptional() + @IsString() + @Length(0, 160) + bio?: string; + + @ApiPropertyOptional({ example: 'https://cdn.example.com/avatar.jpg' }) + @IsOptional() + @IsUrl({ require_tld: false }) + avatar?: string; + + @ApiPropertyOptional({ example: 'Riyadh, Saudi Arabia' }) + @IsOptional() + @IsString() + @Length(0, 120) + location?: string; + + @ApiPropertyOptional({ default: false }) + @IsOptional() + @IsBoolean() + isPrivate?: boolean; + + @ApiPropertyOptional({ enum: MusicRole, isArray: true }) + @IsOptional() + @IsArray() + @IsEnum(MusicRole, { each: true }) + musicRoles?: MusicRole[]; + + @ApiPropertyOptional({ enum: ExperienceLevel }) + @IsOptional() + @IsEnum(ExperienceLevel) + experienceLevel?: ExperienceLevel; + + @ApiPropertyOptional({ type: [String], example: ['Tarab', 'Pop'] }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + musicGenres?: string[]; + + @ApiPropertyOptional({ type: [String], example: ['Oud', 'Piano'] }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + favoriteInstruments?: string[]; + + @ApiPropertyOptional({ type: [String], example: ['Bayati', 'Rast'] }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + favoriteMaqamat?: string[]; +} diff --git a/src/modules/users/dto/user-query.dto.ts b/src/modules/users/dto/user-query.dto.ts new file mode 100644 index 0000000..25c1ac1 --- /dev/null +++ b/src/modules/users/dto/user-query.dto.ts @@ -0,0 +1,15 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsBoolean, IsOptional, IsString } from 'class-validator'; +import { PaginationQueryDto } from '../../../common/dto/pagination-query.dto'; + +export class UserQueryDto extends PaginationQueryDto { + @ApiPropertyOptional({ description: 'Search by name or username' }) + @IsOptional() + @IsString() + q?: string; + + @ApiPropertyOptional({ default: false }) + @IsOptional() + @IsBoolean() + isVerified?: boolean; +} diff --git a/src/modules/users/schemas/user.schema.ts b/src/modules/users/schemas/user.schema.ts new file mode 100644 index 0000000..8f58c25 --- /dev/null +++ b/src/modules/users/schemas/user.schema.ts @@ -0,0 +1,124 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { HydratedDocument } from 'mongoose'; +import { ExperienceLevel } from '../../../common/enums/experience-level.enum'; +import { MusicRole } from '../../../common/enums/music-role.enum'; +import { UserRole } from '../../../common/enums/user-role.enum'; + +export type UserDocument = HydratedDocument; + +@Schema({ timestamps: true, versionKey: false }) +export class User { + @Prop({ required: true, trim: true, minlength: 2, maxlength: 80 }) + name!: string; + + @Prop({ default: '', trim: true, maxlength: 80, index: true }) + stageName!: string; + + @Prop({ required: true, trim: true, lowercase: true, unique: true, index: true, minlength: 3, maxlength: 30 }) + username!: string; + + @Prop({ required: true, trim: true, lowercase: true, unique: true, index: true }) + email!: string; + + @Prop({ required: true, minlength: 8, select: false }) + password!: string; + + @Prop({ type: String, required: false, unique: true, sparse: true, index: true }) + googleId?: string; + + @Prop({ default: 'local', enum: ['local', 'google'], index: true }) + authProvider!: 'local' | 'google'; + + @Prop({ type: String, enum: Object.values(UserRole), default: UserRole.USER, index: true }) + role!: UserRole; + + @Prop({ default: false, index: true }) + isDisabled!: boolean; + + @Prop({ type: Date, required: false }) + disabledAt?: Date; + + @Prop({ default: '', maxlength: 300 }) + disabledReason!: string; + + @Prop({ type: String, required: false, index: true }) + disabledBy?: string; + + @Prop({ default: '', maxlength: 160 }) + bio!: string; + + @Prop({ default: '' }) + avatar!: string; + + @Prop({ default: '' }) + location!: string; + + @Prop({ type: Number, min: -90, max: 90, default: null }) + latitude!: number | null; + + @Prop({ type: Number, min: -180, max: 180, default: null }) + longitude!: number | null; + + @Prop({ type: [String], enum: Object.values(MusicRole), default: [] }) + musicRoles!: MusicRole[]; + + @Prop({ type: String, enum: Object.values(ExperienceLevel), default: ExperienceLevel.BEGINNER }) + experienceLevel!: ExperienceLevel; + + @Prop({ type: [String], default: [] }) + musicGenres!: string[]; + + @Prop({ type: [String], default: [] }) + favoriteInstruments!: string[]; + + @Prop({ type: [String], default: [] }) + favoriteMaqamat!: string[]; + + @Prop({ default: 0, min: 0 }) + followersCount!: number; + + @Prop({ default: 0, min: 0 }) + followingCount!: number; + + @Prop({ default: 0, min: 0 }) + postsCount!: number; + + @Prop({ default: false }) + isPrivate!: boolean; + + @Prop({ default: false, index: true }) + isVerified!: boolean; +} + +export const UserSchema = SchemaFactory.createForClass(User); + +UserSchema.index({ createdAt: -1 }); + +const resolveAvatarUrl = (avatar: unknown): unknown => { + if (typeof avatar !== 'string' || !avatar.trim()) { + return avatar; + } + + if (!avatar.startsWith('/uploads/')) { + return avatar; + } + + const baseUrl = (process.env.PUBLIC_BASE_URL ?? '').replace(/\/$/, ''); + if (!baseUrl) { + return avatar; + } + + return `${baseUrl}${avatar}`; +}; + +const stripLegacyRoleFlags = (_doc: unknown, ret: any) => { + delete ret.isInstrumentalist; + delete ret.isSinger; + delete ret.isComposer; + delete ret.isLyricist; + ret.avatar = resolveAvatarUrl(ret.avatar); + return ret; +}; + +UserSchema.set('toJSON', { transform: stripLegacyRoleFlags }); +UserSchema.set('toObject', { transform: stripLegacyRoleFlags }); diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts new file mode 100644 index 0000000..2f94a05 --- /dev/null +++ b/src/modules/users/users.controller.ts @@ -0,0 +1,188 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, + Query, + UploadedFile, + UseGuards, + UseInterceptors, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { ApiBearerAuth, ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger'; +import { CurrentUser } from '../../common/decorators/current-user.decorator'; +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; +import { SuperAdminJwtAuthGuard } from '../../common/guards/super-admin-jwt-auth.guard'; +import { JwtPayload } from '../../common/interfaces/jwt-payload.interface'; +import { AdminDisableUserDto } from './dto/admin-disable-user.dto'; +import { CreateAdminDto } from './dto/create-admin.dto'; +import { MusicSetupDto } from './dto/music-setup.dto'; +import { ProfileSetupDto } from './dto/profile-setup.dto'; +import { UpdateUserRoleDto } from './dto/update-user-role.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; +import { UserQueryDto } from './dto/user-query.dto'; +import { UsersService } from './users.service'; + +@ApiTags('Users') +@Controller('users') +export class UsersController { + constructor(private readonly usersService: UsersService) {} + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Patch('me/profile-setup') + @UseInterceptors(FileInterceptor('avatarFile')) + @ApiConsumes('application/json', 'multipart/form-data') + @ApiBody({ + schema: { + type: 'object', + properties: { + stageName: { type: 'string', example: 'Artist One' }, + bio: { type: 'string', example: 'Short bio' }, + location: { type: 'string', example: 'Riyadh, Saudi Arabia' }, + latitude: { type: 'number', example: 24.7136 }, + longitude: { type: 'number', example: 46.6753 }, + avatar: { type: 'string', example: 'https://cdn.example.com/avatar.jpg' }, + avatarFile: { type: 'string', format: 'binary' }, + }, + required: ['latitude', 'longitude'], + }, + }) + async updateProfileSetup( + @CurrentUser() user: JwtPayload, + @Body() dto: ProfileSetupDto, + @UploadedFile() + avatarFile?: { mimetype?: string; size: number; buffer: Buffer; originalname?: string }, + ) { + return this.usersService.updateProfileSetup(user.sub, dto, avatarFile); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Patch('me/music-setup') + async updateMusicSetup(@CurrentUser() user: JwtPayload, @Body() dto: MusicSetupDto) { + return this.usersService.updateMusicSetup(user.sub, dto); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Patch('me') + async updateMe(@CurrentUser() user: JwtPayload, @Body() dto: UpdateUserDto) { + return this.usersService.updateProfile(user.sub, dto); + } + + @ApiBearerAuth() + @UseGuards(SuperAdminJwtAuthGuard) + @Post('admin/create-admin') + async createAdmin( + @CurrentUser() user: JwtPayload, + @Body() dto: CreateAdminDto, + ) { + return this.usersService.createAdminBySuperAdmin(user.email ?? user.sub, dto); + } + + @ApiBearerAuth() + @UseGuards(SuperAdminJwtAuthGuard) + @Get('admin') + async adminFindMany(@Query() query: UserQueryDto) { + return this.usersService.searchUsersForSuperAdmin(query); + } + + @ApiBearerAuth() + @UseGuards(SuperAdminJwtAuthGuard) + @Get('admin/admins') + async adminListAdmins(@Query() query: UserQueryDto) { + return this.usersService.listAdminsBySuperAdmin(query); + } + + @ApiBearerAuth() + @UseGuards(SuperAdminJwtAuthGuard) + @Get('admin/:id') + async adminFindOne(@Param('id') targetUserId: string) { + return this.usersService.findUserByIdForSuperAdmin(targetUserId); + } + + @ApiBearerAuth() + @UseGuards(SuperAdminJwtAuthGuard) + @Patch('admin/:id') + async adminUpdateUser( + @CurrentUser() user: JwtPayload, + @Param('id') targetUserId: string, + @Body() dto: UpdateUserDto, + ) { + return this.usersService.updateUserBySuperAdmin(user.email ?? user.sub, targetUserId, dto); + } + + @ApiBearerAuth() + @UseGuards(SuperAdminJwtAuthGuard) + @Patch('admin/:id/role') + async adminUpdateUserRole( + @CurrentUser() user: JwtPayload, + @Param('id') targetUserId: string, + @Body() dto: UpdateUserRoleDto, + ) { + return this.usersService.updateUserRoleBySuperAdmin(user.email ?? user.sub, targetUserId, dto); + } + + @ApiBearerAuth() + @UseGuards(SuperAdminJwtAuthGuard) + @Patch('admin/:id/disable') + async disableUser( + @CurrentUser() user: JwtPayload, + @Param('id') targetUserId: string, + @Body() dto: AdminDisableUserDto, + ) { + return this.usersService.disableUserBySuperAdmin(user.email ?? user.sub, targetUserId, dto); + } + + @ApiBearerAuth() + @UseGuards(SuperAdminJwtAuthGuard) + @Patch('admin/:id/enable') + async enableUser(@CurrentUser() user: JwtPayload, @Param('id') targetUserId: string) { + return this.usersService.enableUserBySuperAdmin(user.email ?? user.sub, targetUserId); + } + + @ApiBearerAuth() + @UseGuards(SuperAdminJwtAuthGuard) + @Delete('admin/:id') + async deleteUser(@CurrentUser() user: JwtPayload, @Param('id') targetUserId: string) { + await this.usersService.deleteUserBySuperAdmin(user.email ?? user.sub, targetUserId); + return { message: 'User deleted successfully' }; + } + + @ApiBearerAuth() + @UseGuards(SuperAdminJwtAuthGuard) + @Patch('admin/admins/:id') + async adminUpdateAdmin( + @CurrentUser() user: JwtPayload, + @Param('id') targetUserId: string, + @Body() dto: UpdateUserDto, + ) { + return this.usersService.updateAdminBySuperAdmin(user.email ?? user.sub, targetUserId, dto); + } + + @ApiBearerAuth() + @UseGuards(SuperAdminJwtAuthGuard) + @Delete('admin/admins/:id') + async adminDeleteAdmin(@CurrentUser() user: JwtPayload, @Param('id') targetUserId: string) { + await this.usersService.deleteAdminBySuperAdmin(user.email ?? user.sub, targetUserId); + return { message: 'Admin deleted successfully' }; + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get(':id') + async findOne(@Param('id') id: string) { + return this.usersService.findByIdOrFail(id); + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Get() + async findMany(@Query() query: UserQueryDto) { + return this.usersService.searchUsers(query); + } +} diff --git a/src/modules/users/users.module.ts b/src/modules/users/users.module.ts new file mode 100644 index 0000000..1442f01 --- /dev/null +++ b/src/modules/users/users.module.ts @@ -0,0 +1,23 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { AuditModule } from '../audit/audit.module'; +import { UsersController } from './users.controller'; +import { UsersService } from './users.service'; +import { UsersRepository } from './users.repository'; +import { User, UserSchema } from './schemas/user.schema'; + +@Module({ + imports: [ + AuditModule, + MongooseModule.forFeature([ + { + name: User.name, + schema: UserSchema, + }, + ]), + ], + controllers: [UsersController], + providers: [UsersService, UsersRepository], + exports: [UsersService, UsersRepository, MongooseModule], +}) +export class UsersModule {} diff --git a/src/modules/users/users.repository.ts b/src/modules/users/users.repository.ts new file mode 100644 index 0000000..af7cc89 --- /dev/null +++ b/src/modules/users/users.repository.ts @@ -0,0 +1,90 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { ClientSession, FilterQuery, Model, UpdateQuery } from 'mongoose'; +import { User, UserDocument } from './schemas/user.schema'; + +@Injectable() +export class UsersRepository { + constructor(@InjectModel(User.name) private readonly userModel: Model) {} + + async create(payload: Partial): Promise { + return this.userModel.create(payload); + } + + async findById(id: string): Promise { + return this.userModel.findById(id).exec(); + } + + async findByIdWithPassword(id: string): Promise { + return this.userModel.findById(id).select('+password').exec(); + } + + async findOne(filter: FilterQuery): Promise { + return this.userModel.findOne(filter).exec(); + } + + async findOneWithPassword(filter: FilterQuery): Promise { + return this.userModel.findOne(filter).select('+password').exec(); + } + + async updateById(id: string, payload: UpdateQuery): Promise { + return this.userModel.findByIdAndUpdate(id, payload, { new: true }).exec(); + } + + async deleteById(id: string): Promise { + return this.userModel.findByIdAndDelete(id).exec(); + } + + async incrementPostsCount(userId: string, delta: 1 | -1, session?: ClientSession): Promise { + await this.userModel + .findByIdAndUpdate(userId, { $inc: { postsCount: delta } }, { new: false, session }) + .exec(); + } + + async incrementFollowersCount( + userId: string, + delta: 1 | -1, + session?: ClientSession, + ): Promise { + await this.userModel + .findByIdAndUpdate(userId, { $inc: { followersCount: delta } }, { new: false, session }) + .exec(); + } + + async incrementFollowingCount( + userId: string, + delta: 1 | -1, + session?: ClientSession, + ): Promise { + await this.userModel + .findByIdAndUpdate(userId, { $inc: { followingCount: delta } }, { new: false, session }) + .exec(); + } + + async setFollowersCount(userId: string, followersCount: number): Promise { + await this.userModel.findByIdAndUpdate(userId, { followersCount }, { new: false }).exec(); + } + + async setFollowingCount(userId: string, followingCount: number): Promise { + await this.userModel.findByIdAndUpdate(userId, { followingCount }, { new: false }).exec(); + } + + async findMany(filter: FilterQuery, skip: number, limit: number): Promise { + return this.userModel.find(filter).sort({ createdAt: -1 }).skip(skip).limit(limit).exec(); + } + + async count(filter: FilterQuery): Promise { + return this.userModel.countDocuments(filter).exec(); + } + + async findSuggestionCandidates( + filter: FilterQuery, + limit: number, + ): Promise { + return this.userModel + .find(filter) + .sort({ isVerified: -1, followersCount: -1, createdAt: -1 }) + .limit(limit) + .exec(); + } +} diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts new file mode 100644 index 0000000..5df49e6 --- /dev/null +++ b/src/modules/users/users.service.ts @@ -0,0 +1,649 @@ +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { InjectConnection } from '@nestjs/mongoose'; +import { ConfigService } from '@nestjs/config'; +import { randomUUID } from 'crypto'; +import { mkdir, unlink, writeFile } from 'fs/promises'; +import { extname, join } from 'path'; +import { Connection, Types } from 'mongoose'; +import { ExperienceLevel } from '../../common/enums/experience-level.enum'; +import { hashValue } from '../../common/utils/hash.util'; +import { AuditService } from '../audit/audit.service'; +import { UserRole } from '../../common/enums/user-role.enum'; +import { CreateUserDto } from './dto/create-user.dto'; +import { AdminDisableUserDto } from './dto/admin-disable-user.dto'; +import { CreateAdminDto } from './dto/create-admin.dto'; +import { MusicSetupDto } from './dto/music-setup.dto'; +import { ProfileSetupDto } from './dto/profile-setup.dto'; +import { UpdateUserRoleDto } from './dto/update-user-role.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; +import { UserQueryDto } from './dto/user-query.dto'; +import { UsersRepository } from './users.repository'; +import { UserDocument } from './schemas/user.schema'; + +@Injectable() +export class UsersService { + constructor( + @InjectConnection() private readonly connection: Connection, + private readonly usersRepository: UsersRepository, + private readonly auditService: AuditService, + private readonly configService: ConfigService, + ) {} + + async create(dto: CreateUserDto & { password: string; role?: UserRole }): Promise { + const existing = await this.usersRepository.findOne({ + $or: [{ email: dto.email.toLowerCase() }, { username: dto.username.toLowerCase() }], + }); + + if (existing) { + throw new BadRequestException('Email or username already exists'); + } + + const roles = dto.musicRoles ?? []; + + return this.usersRepository.create({ + ...dto, + email: dto.email.toLowerCase(), + username: dto.username.toLowerCase(), + stageName: dto.stageName ?? '', + bio: dto.bio ?? '', + avatar: dto.avatar ?? '', + location: dto.location ?? '', + latitude: dto.latitude, + longitude: dto.longitude, + isPrivate: dto.isPrivate ?? false, + isVerified: dto.isVerified ?? false, + musicRoles: roles, + experienceLevel: dto.experienceLevel ?? ExperienceLevel.BEGINNER, + musicGenres: dto.musicGenres ?? [], + favoriteInstruments: dto.favoriteInstruments ?? [], + favoriteMaqamat: dto.favoriteMaqamat ?? [], + role: dto.role ?? UserRole.USER, + isDisabled: false, + disabledReason: '', + authProvider: 'local', + }); + } + + async createAdminBySuperAdmin( + superAdminIdentifier: string, + dto: CreateAdminDto, + ): Promise { + if (dto.password !== dto.confirmPassword) { + throw new BadRequestException('Password confirmation does not match'); + } + + const saltRounds = this.configService.get('security.bcryptSaltRounds', { infer: true }); + const passwordHash = await hashValue(dto.password, saltRounds); + + const admin = await this.create({ + name: dto.name, + username: dto.username, + email: dto.email, + password: passwordHash, + role: UserRole.ADMIN, + isVerified: true, + }); + + await this.auditService.logSuperAdminAction( + superAdminIdentifier, + 'admin_create', + 'user', + admin.id, + { role: UserRole.ADMIN }, + ); + + return admin; + } + + async listAdminsBySuperAdmin(query: UserQueryDto): Promise<{ + items: UserDocument[]; + page: number; + limit: number; + total: number; + totalPages: number; + }> { + const page = query.page ?? 1; + const limit = query.limit ?? 20; + const skip = (page - 1) * limit; + + const filter: Record = { + role: UserRole.ADMIN, + }; + + if (query.q) { + filter.$or = [ + { name: { $regex: query.q, $options: 'i' } }, + { username: { $regex: query.q, $options: 'i' } }, + { email: { $regex: query.q, $options: 'i' } }, + ]; + } + + if (typeof query.isVerified === 'boolean') { + filter.isVerified = query.isVerified; + } + + const [items, total] = await Promise.all([ + this.usersRepository.findMany(filter, skip, limit), + this.usersRepository.count(filter), + ]); + + return { + items, + page, + limit, + total, + totalPages: Math.ceil(total / limit) || 1, + }; + } + + async updateAdminBySuperAdmin( + superAdminIdentifier: string, + adminUserId: string, + dto: UpdateUserDto, + ): Promise { + await this.assertTargetIsAdmin(adminUserId); + + const updated = await this.usersRepository.updateById(adminUserId, dto); + if (!updated) { + throw new NotFoundException('Admin not found'); + } + + await this.auditService.logSuperAdminAction( + superAdminIdentifier, + 'admin_update', + 'user', + adminUserId, + { fields: Object.keys(dto) }, + ); + + return updated; + } + + async deleteAdminBySuperAdmin(superAdminIdentifier: string, adminUserId: string): Promise { + const admin = await this.assertTargetIsAdmin(adminUserId); + await this.deleteUserRelatedData(adminUserId, admin.avatar ?? ''); + + await this.usersRepository.deleteById(adminUserId); + + await this.auditService.logSuperAdminAction( + superAdminIdentifier, + 'admin_delete', + 'user', + adminUserId, + ); + } + + async updateUserRoleBySuperAdmin( + superAdminIdentifier: string, + targetUserId: string, + dto: UpdateUserRoleDto, + ): Promise { + if (dto.role === UserRole.SUPERADMIN) { + throw new BadRequestException('Cannot assign superadmin role via API'); + } + + const updated = await this.usersRepository.updateById(targetUserId, { role: dto.role }); + if (!updated) { + throw new NotFoundException('User not found'); + } + + await this.auditService.logSuperAdminAction( + superAdminIdentifier, + 'user_role_update', + 'user', + targetUserId, + { role: dto.role }, + ); + + return updated; + } + + async findByIdOrFail(userId: string): Promise { + const user = await this.usersRepository.findById(userId); + if (!user) { + throw new NotFoundException('User not found'); + } + return user; + } + + async findByEmailWithPassword(email: string): Promise { + return this.usersRepository.findOneWithPassword({ email: email.toLowerCase() }); + } + + async findByEmail(email: string): Promise { + return this.usersRepository.findOne({ email: email.toLowerCase() }); + } + + async findByUsername(username: string): Promise { + return this.usersRepository.findOne({ username: username.toLowerCase() }); + } + + async findByGoogleId(googleId: string): Promise { + return this.usersRepository.findOne({ googleId }); + } + + async linkGoogleAccount(userId: string, googleId: string, avatar?: string): Promise { + const user = await this.usersRepository.updateById(userId, { + googleId, + authProvider: 'google', + ...(avatar ? { avatar } : {}), + }); + + if (!user) { + throw new NotFoundException('User not found'); + } + + return user; + } + + async updateProfile(userId: string, dto: UpdateUserDto): Promise { + const user = await this.usersRepository.updateById(userId, dto); + if (!user) { + throw new NotFoundException('User not found'); + } + return user; + } + + async updatePassword(userId: string, passwordHash: string): Promise { + const updated = await this.usersRepository.updateById(userId, { password: passwordHash }); + if (!updated) { + throw new NotFoundException('User not found'); + } + } + + async markEmailVerified(userId: string): Promise { + const updated = await this.usersRepository.updateById(userId, { isVerified: true }); + if (!updated) { + throw new NotFoundException('User not found'); + } + } + + async findUserByIdForSuperAdmin(targetUserId: string): Promise { + return this.findByIdOrFail(targetUserId); + } + + async updateUserBySuperAdmin( + superAdminIdentifier: string, + targetUserId: string, + dto: UpdateUserDto, + ): Promise { + const user = await this.usersRepository.updateById(targetUserId, dto); + if (!user) { + throw new NotFoundException('User not found'); + } + await this.auditService.logSuperAdminAction( + superAdminIdentifier, + 'user_update', + 'user', + targetUserId, + { fields: Object.keys(dto) }, + ); + return user; + } + + async updateProfileSetup( + userId: string, + dto: ProfileSetupDto, + avatarFile?: { mimetype?: string; size: number; buffer: Buffer; originalname?: string }, + ): Promise { + if (!Number.isFinite(dto.latitude) || !Number.isFinite(dto.longitude)) { + throw new BadRequestException('latitude and longitude are required'); + } + + const currentUser = await this.findByIdOrFail(userId); + const payload: Record = { ...dto }; + let uploadedAvatarUrl: string | null = null; + + if (avatarFile) { + uploadedAvatarUrl = await this.saveAvatarFile(avatarFile); + payload.avatar = uploadedAvatarUrl; + } + + const user = await this.usersRepository.updateById(userId, payload); + if (!user) { + if (uploadedAvatarUrl) { + await this.deleteManagedAvatar(uploadedAvatarUrl); + } + throw new NotFoundException('User not found'); + } + + const nextAvatar = + typeof payload.avatar === 'string' ? payload.avatar : currentUser.avatar; + + if (nextAvatar !== currentUser.avatar) { + await this.deleteManagedAvatar(currentUser.avatar); + } + + return user; + } + + private async saveAvatarFile( + avatarFile: { mimetype?: string; size: number; buffer: Buffer; originalname?: string }, + ): Promise { + const extension = this.resolveAvatarExtension(avatarFile); + const maxSize = 5 * 1024 * 1024; + + if (!extension) { + throw new BadRequestException('avatarFile must be png, jpg, jpeg, webp, or gif'); + } + + if (avatarFile.size > maxSize) { + throw new BadRequestException('avatarFile size must be 5MB or less'); + } + + const uploadDir = join(process.cwd(), 'uploads', 'avatars'); + const fileName = `${randomUUID()}${extension}`; + + await mkdir(uploadDir, { recursive: true }); + await writeFile(join(uploadDir, fileName), avatarFile.buffer); + + return `/uploads/avatars/${encodeURIComponent(fileName)}`; + } + + private resolveAvatarExtension(avatarFile: { + mimetype?: string; + originalname?: string; + }): string | null { + const originalExtension = extname(avatarFile.originalname ?? '').toLowerCase(); + const allowedExtensions = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif']); + + if (allowedExtensions.has(originalExtension)) { + return originalExtension; + } + + switch (avatarFile.mimetype) { + case 'image/png': + return '.png'; + case 'image/jpeg': + case 'image/jpg': + return '.jpg'; + case 'image/webp': + return '.webp'; + case 'image/gif': + return '.gif'; + default: + return null; + } + } + + private async deleteManagedAvatar(avatarUrl?: string): Promise { + const marker = '/uploads/avatars/'; + const markerIndex = avatarUrl?.indexOf(marker) ?? -1; + + if (markerIndex === -1) { + return; + } + + const encodedFileName = avatarUrl! + .slice(markerIndex + marker.length) + .split('?')[0] + .split('#')[0]; + + if (!encodedFileName) { + return; + } + + const fileName = decodeURIComponent(encodedFileName); + + try { + await unlink(join(process.cwd(), 'uploads', 'avatars', fileName)); + } catch { + // Ignore cleanup failures for already-missing files. + } + } + + async updateMusicSetup(userId: string, dto: MusicSetupDto): Promise { + const payload: Record = { ...dto }; + + const user = await this.usersRepository.updateById(userId, payload); + if (!user) { + throw new NotFoundException('User not found'); + } + return user; + } + + async disableUserBySuperAdmin( + superAdminIdentifier: string, + targetUserId: string, + dto: AdminDisableUserDto, + ): Promise { + await this.findByIdOrFail(targetUserId); + + const updated = await this.usersRepository.updateById(targetUserId, { + isDisabled: true, + disabledAt: new Date(), + disabledReason: dto.reason ?? '', + disabledBy: superAdminIdentifier, + }); + if (!updated) { + throw new NotFoundException('User not found'); + } + await this.auditService.logSuperAdminAction( + superAdminIdentifier, + 'user_disable', + 'user', + targetUserId, + { reason: dto.reason ?? '' }, + ); + return updated; + } + + async enableUserBySuperAdmin(superAdminIdentifier: string, targetUserId: string): Promise { + const updated = await this.usersRepository.updateById(targetUserId, { + isDisabled: false, + disabledAt: null, + disabledReason: '', + disabledBy: null, + }); + if (!updated) { + throw new NotFoundException('User not found'); + } + await this.auditService.logSuperAdminAction( + superAdminIdentifier, + 'user_enable', + 'user', + targetUserId, + ); + return updated; + } + + async deleteUserBySuperAdmin(superAdminIdentifier: string, targetUserId: string): Promise { + const user = await this.findByIdOrFail(targetUserId); + await this.deleteUserRelatedData(targetUserId, user.avatar ?? ''); + await this.usersRepository.deleteById(targetUserId); + await this.auditService.logSuperAdminAction( + superAdminIdentifier, + 'user_delete', + 'user', + targetUserId, + ); + } + + async searchUsers(query: UserQueryDto): Promise<{ + items: UserDocument[]; + page: number; + limit: number; + total: number; + totalPages: number; + }> { + const page = query.page ?? 1; + const limit = query.limit ?? 20; + const skip = (page - 1) * limit; + + const filter: Record = {}; + + if (query.q) { + filter.$or = [ + { name: { $regex: query.q, $options: 'i' } }, + { username: { $regex: query.q, $options: 'i' } }, + ]; + } + + if (typeof query.isVerified === 'boolean') { + filter.isVerified = query.isVerified; + } + + const [items, total] = await Promise.all([ + this.usersRepository.findMany(filter, skip, limit), + this.usersRepository.count(filter), + ]); + + return { + items, + page, + limit, + total, + totalPages: Math.ceil(total / limit) || 1, + }; + } + + async searchUsersForSuperAdmin(query: UserQueryDto): Promise<{ + items: UserDocument[]; + page: number; + limit: number; + total: number; + totalPages: number; + }> { + return this.searchUsers(query); + } + + private async assertTargetIsAdmin(userId: string): Promise { + const user = await this.findByIdOrFail(userId); + if (user.role !== UserRole.ADMIN) { + throw new BadRequestException('Target user is not an admin'); + } + return user; + } + + private async deleteUserRelatedData(userId: string, avatarUrl?: string): Promise { + if (!Types.ObjectId.isValid(userId)) { + return; + } + + const objectUserId = new Types.ObjectId(userId); + + const postsCollection = this.connection.collection('posts'); + const commentsCollection = this.connection.collection('comments'); + const likesCollection = this.connection.collection('likes'); + const savesCollection = this.connection.collection('saves'); + const followsCollection = this.connection.collection('follows'); + const notificationsCollection = this.connection.collection('notifications'); + const conversationsCollection = this.connection.collection('conversations'); + const messagesCollection = this.connection.collection('messages'); + const chatBlocksCollection = this.connection.collection('chatblocks'); + const marketplaceCollection = this.connection.collection('instruments'); + const refreshTokensCollection = this.connection.collection('refreshtokens'); + const passwordResetCodesCollection = this.connection.collection('passwordresetcodes'); + const emailVerificationCodesCollection = this.connection.collection('emailverificationcodes'); + + const [adminPosts, ownComments, adminConversations, ownMessages, ownInstruments] = await Promise.all([ + postsCollection.find({ authorId: objectUserId }).project({ _id: 1, videoUrl: 1, audioUrl: 1 }).toArray(), + commentsCollection.find({ authorId: objectUserId }).project({ _id: 1 }).toArray(), + conversationsCollection.find({ participantIds: objectUserId }).project({ _id: 1 }).toArray(), + messagesCollection.find({ senderId: objectUserId }).project({ _id: 1, mediaUrl: 1 }).toArray(), + marketplaceCollection.find({ ownerAdminId: objectUserId }).project({ _id: 1, imageUrls: 1 }).toArray(), + ]); + + const postIds = adminPosts.map((post) => post._id as Types.ObjectId); + const ownCommentIds = ownComments.map((comment) => comment._id as Types.ObjectId); + const conversationIds = adminConversations.map((conversation) => conversation._id as Types.ObjectId); + + const postComments = postIds.length + ? await commentsCollection.find({ postId: { $in: postIds } }).project({ _id: 1 }).toArray() + : []; + const postCommentIds = postComments.map((comment) => comment._id as Types.ObjectId); + const commentIds = [...ownCommentIds, ...postCommentIds]; + + const conversationMessages = conversationIds.length + ? await messagesCollection + .find({ conversationId: { $in: conversationIds } }) + .project({ _id: 1, mediaUrl: 1 }) + .toArray() + : []; + + const localFilesToDelete = new Set(); + if (avatarUrl) { + localFilesToDelete.add(avatarUrl); + } + + for (const post of adminPosts) { + if (typeof post.videoUrl === 'string') { + localFilesToDelete.add(post.videoUrl); + } + if (typeof post.audioUrl === 'string') { + localFilesToDelete.add(post.audioUrl); + } + } + + for (const message of [...ownMessages, ...conversationMessages]) { + if (typeof message.mediaUrl === 'string') { + localFilesToDelete.add(message.mediaUrl); + } + } + + for (const instrument of ownInstruments) { + if (Array.isArray(instrument.imageUrls)) { + for (const imageUrl of instrument.imageUrls) { + if (typeof imageUrl === 'string') { + localFilesToDelete.add(imageUrl); + } + } + } + } + + const likesDeleteOr: Record[] = [{ userId: objectUserId }]; + if (postIds.length) { + likesDeleteOr.push({ targetType: 'post', targetId: { $in: postIds } }); + } + if (commentIds.length) { + likesDeleteOr.push({ targetType: 'comment', targetId: { $in: commentIds } }); + } + + const savesDeleteOr: Record[] = [{ userId: objectUserId }]; + if (postIds.length) { + savesDeleteOr.push({ postId: { $in: postIds } }); + } + + await Promise.all([ + likesCollection.deleteMany({ $or: likesDeleteOr }), + savesCollection.deleteMany({ $or: savesDeleteOr }), + followsCollection.deleteMany({ $or: [{ followerId: objectUserId }, { followingId: objectUserId }] }), + notificationsCollection.deleteMany({ $or: [{ recipientId: objectUserId }, { actorId: objectUserId }] }), + chatBlocksCollection.deleteMany({ $or: [{ blockerId: objectUserId }, { blockedId: objectUserId }] }), + refreshTokensCollection.deleteMany({ userId: objectUserId }), + passwordResetCodesCollection.deleteMany({ userId: objectUserId }), + emailVerificationCodesCollection.deleteMany({ userId: objectUserId }), + conversationIds.length ? messagesCollection.deleteMany({ conversationId: { $in: conversationIds } }) : null, + ownMessages.length ? messagesCollection.deleteMany({ senderId: objectUserId }) : null, + conversationIds.length ? conversationsCollection.deleteMany({ _id: { $in: conversationIds } }) : null, + commentIds.length ? commentsCollection.deleteMany({ _id: { $in: commentIds } }) : null, + postIds.length ? postsCollection.deleteMany({ _id: { $in: postIds } }) : null, + marketplaceCollection.deleteMany({ ownerAdminId: objectUserId }), + ]); + + await Promise.all([...localFilesToDelete].map((fileUrl) => this.deleteManagedUpload(fileUrl))); + } + + private async deleteManagedUpload(fileUrl: string): Promise { + if (!fileUrl?.startsWith('/uploads/')) { + return; + } + + const relativePath = fileUrl.split('?')[0].split('#')[0].replace(/^\/+/, ''); + const normalizedPath = relativePath.replace(/\//g, '\\'); + + if (normalizedPath.includes('..')) { + return; + } + + try { + await unlink(join(process.cwd(), normalizedPath)); + } catch { + // Ignore cleanup failures for already-missing files. + } + } +} diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts new file mode 100644 index 0000000..bbffb13 --- /dev/null +++ b/test/app.e2e-spec.ts @@ -0,0 +1,21 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { AppModule } from '../src/app.module'; + +describe('AppController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/health (GET)', () => { + return request(app.getHttpServer()).get('/health').expect(200); + }); +}); diff --git a/test/jest-e2e.json b/test/jest-e2e.json new file mode 100644 index 0000000..e9d912f --- /dev/null +++ b/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..64f86c6 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2dfda9f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true + } +}