From 05f3a2fe1988e9adb0c823abaf1343a4a152230b Mon Sep 17 00:00:00 2001 From: Gabriel Mata Date: Fri, 24 Jan 2025 16:35:02 -0500 Subject: [PATCH 01/15] feat: Introduce loadbalancer to webrtc servers instances --- .gitignore | 12 +- .prettierignore | 1 + .release-please-manifest.json | 4 + LICENSE | 201 + README.md | 84 +- .../src/__pycache__/mediasoup.cpython-310.pyc | Bin 0 -> 11829 bytes jest.config.ts | 3 +- nest-cli.json | 28 +- package.json | 19 +- packages/loadbalancer/jest.config.ts | 21 + packages/loadbalancer/nest-cli.json | 8 + packages/loadbalancer/package.json | 59 + .../loadbalancer/src}/config/app.config.ts | 0 .../loadbalancer/src}/config/logger.config.ts | 0 packages/loadbalancer/src/dto/rooms.dto.ts | 105 + .../src/lib/HttpRequestService.ts | 58 + .../src/loadbalancer.controller.spec.ts | 22 + .../src/loadbalancer.controller.ts | 97 + .../loadbalancer/src/loadbalancer.module.ts | 20 + .../loadbalancer/src/loadbalancer.service.ts | 179 + packages/loadbalancer/src/main.ts | 56 + .../loadbalancer/src}/modules/redis.module.ts | 0 packages/loadbalancer/test/app.e2e-spec.ts | 24 + packages/loadbalancer/test/jest-e2e.json | 9 + packages/loadbalancer/tsconfig.app.json | 9 + packages/loadbalancer/tsconfig.build.json | 27 + packages/loadbalancer/tsconfig.json | 12 + .../webrtc-server/Dockerfile | 0 .../webrtc-server/docker-compose-lb.yml | 0 .../webrtc-server/docker-compose.yml | 5 +- packages/webrtc-server/jest.config.ts | 21 + packages/webrtc-server/nest-cli.json | 9 + packages/webrtc-server/package.json | 73 + .../webrtc-server/src}/app.module.ts | 0 .../webrtc-server/src/config/app.config.ts | 43 + .../src}/config/config.server.ts | 0 .../webrtc-server/src}/config/constants.ts | 0 .../webrtc-server/src/config/logger.config.ts | 26 + .../webrtc-server/src}/lib/Room.ts | 2 +- .../webrtc-server/src}/lib/RoomFactory.ts | 0 .../src}/lib/notification.service.ts | 0 .../webrtc-server/src}/lib/room.interfaces.ts | 0 {src => packages/webrtc-server/src}/main.ts | 2 +- .../webrtc-server/src/modules/redis.module.ts | 79 + .../webrtc-server/src}/rooms/dto/rooms.dto.ts | 0 .../src}/rooms/rooms.controller.spec.ts | 0 .../src}/rooms/rooms.controller.ts | 0 .../webrtc-server/src}/rooms/rooms.module.ts | 0 .../src}/rooms/rooms.service.spec.ts | 4 +- .../webrtc-server/src}/rooms/rooms.service.ts | 0 .../webrtc-server/test}/app.e2e-spec.ts | 4 +- .../webrtc-server/test}/jest-e2e.json | 0 packages/webrtc-server/tsconfig.app.json | 9 + packages/webrtc-server/tsconfig.build.json | 27 + packages/webrtc-server/tsconfig.json | 12 + packages/webrtc-server/yarn.lock | 5699 +++++++++++++++++ release-please-config.json | 17 + tsconfig.build.json | 27 +- tsconfig.json | 28 +- yarn.lock | 106 +- 60 files changed, 7144 insertions(+), 107 deletions(-) create mode 100644 .prettierignore create mode 100644 .release-please-manifest.json create mode 100644 LICENSE create mode 100644 demo/pymediasoup-client/src/__pycache__/mediasoup.cpython-310.pyc create mode 100644 packages/loadbalancer/jest.config.ts create mode 100644 packages/loadbalancer/nest-cli.json create mode 100644 packages/loadbalancer/package.json rename {src => packages/loadbalancer/src}/config/app.config.ts (100%) rename {src => packages/loadbalancer/src}/config/logger.config.ts (100%) create mode 100644 packages/loadbalancer/src/dto/rooms.dto.ts create mode 100644 packages/loadbalancer/src/lib/HttpRequestService.ts create mode 100644 packages/loadbalancer/src/loadbalancer.controller.spec.ts create mode 100644 packages/loadbalancer/src/loadbalancer.controller.ts create mode 100644 packages/loadbalancer/src/loadbalancer.module.ts create mode 100644 packages/loadbalancer/src/loadbalancer.service.ts create mode 100644 packages/loadbalancer/src/main.ts rename {src => packages/loadbalancer/src}/modules/redis.module.ts (100%) create mode 100644 packages/loadbalancer/test/app.e2e-spec.ts create mode 100644 packages/loadbalancer/test/jest-e2e.json create mode 100644 packages/loadbalancer/tsconfig.app.json create mode 100644 packages/loadbalancer/tsconfig.build.json create mode 100644 packages/loadbalancer/tsconfig.json rename Dockerfile => packages/webrtc-server/Dockerfile (100%) rename docker-compose-lb.yml => packages/webrtc-server/docker-compose-lb.yml (100%) rename docker-compose.yml => packages/webrtc-server/docker-compose.yml (89%) create mode 100644 packages/webrtc-server/jest.config.ts create mode 100644 packages/webrtc-server/nest-cli.json create mode 100644 packages/webrtc-server/package.json rename {src => packages/webrtc-server/src}/app.module.ts (100%) create mode 100644 packages/webrtc-server/src/config/app.config.ts rename {src => packages/webrtc-server/src}/config/config.server.ts (100%) rename {src => packages/webrtc-server/src}/config/constants.ts (100%) create mode 100644 packages/webrtc-server/src/config/logger.config.ts rename {src => packages/webrtc-server/src}/lib/Room.ts (99%) rename {src => packages/webrtc-server/src}/lib/RoomFactory.ts (100%) rename {src => packages/webrtc-server/src}/lib/notification.service.ts (100%) rename {src => packages/webrtc-server/src}/lib/room.interfaces.ts (100%) rename {src => packages/webrtc-server/src}/main.ts (98%) create mode 100644 packages/webrtc-server/src/modules/redis.module.ts rename {src => packages/webrtc-server/src}/rooms/dto/rooms.dto.ts (100%) rename {src => packages/webrtc-server/src}/rooms/rooms.controller.spec.ts (100%) rename {src => packages/webrtc-server/src}/rooms/rooms.controller.ts (100%) rename {src => packages/webrtc-server/src}/rooms/rooms.module.ts (100%) rename {src => packages/webrtc-server/src}/rooms/rooms.service.spec.ts (99%) rename {src => packages/webrtc-server/src}/rooms/rooms.service.ts (100%) rename {test => packages/webrtc-server/test}/app.e2e-spec.ts (97%) rename {test => packages/webrtc-server/test}/jest-e2e.json (100%) create mode 100644 packages/webrtc-server/tsconfig.app.json create mode 100644 packages/webrtc-server/tsconfig.build.json create mode 100644 packages/webrtc-server/tsconfig.json create mode 100644 packages/webrtc-server/yarn.lock create mode 100644 release-please-config.json diff --git a/.gitignore b/.gitignore index 5e956cf..3703f67 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # compiled output -/dist -/node_modules -/certs +dist +node_modules +certs # Logs logs @@ -33,4 +33,8 @@ lerna-debug.log* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json -!.vscode/extensions.json \ No newline at end of file +!.vscode/extensions.json + +#files +package-lock.json +packages/server/yarn.lock diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..83b6947 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +CHANGELOG.md \ No newline at end of file diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..221042c --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,4 @@ +{ + "packages/loadbalancer": "0.0.1", + "packages/webrtc-server": "0.0.1" +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 6d65ccb..e1b583b 100644 --- a/README.md +++ b/README.md @@ -35,29 +35,28 @@ You can deploy it using Docker or Kubernetes, with integration of the [Coturn](h To configure and build the `ICE Server` you can be use following enviroment variables: -| Variable | Description | Default Value | -| ----------------------------------- | ----------------------------------------------- | ------------- | -| `MEDIASOUP_CLIENT_PROTOOPORT` | Port used for the connection. | `443` | -| `MEDIASOUP_CLIENT_ENABLE_ICESERVER` | Enable ICE server mode. | `yes` | -| `MEDIASOUP_CLIENT_ICESERVER_PROTO` | Protocol configuration used (e.g., `udp`). | `udp` | -| `MEDIASOUP_CLIENT_ICESERVER_PORT` | Port set in the TURN server to receive traffic. | `3478` | -| `MEDIASOUP_CLIENT_ICESERVER_USER` | Username for the TURN server. | | -| `MEDIASOUP_CLIENT_ICESERVER_PASS` | Password for the TURN server. | | -| `MEDIASOUP_CLIENT_ICESERVER_HOST` | Public IP address of the TURN server. | | +| Variable | Description | Default Value | +| ---------------------------------- | ----------------------------------------------- | ------------- | +| `MEDIASOUP_CLIENT_PROTOOPORT` | Port used for the connection. | `443` | +| `MEDIASOUP_CLIENT_ICESERVER_PROTO` | Protocol configuration used (e.g., `udp`). | `udp` | +| `MEDIASOUP_CLIENT_ICESERVER_PORT` | Port set in the TURN server to receive traffic. | `3478` | +| `MEDIASOUP_CLIENT_ICESERVER_USER` | Username for the TURN server. | | +| `MEDIASOUP_CLIENT_ICESERVER_PASS` | Password for the TURN server. | | +| `MEDIASOUP_CLIENT_ICESERVER_HOST` | Public IP address of the TURN server. | | Additional variables for configuring the `webrtc-server`: -| Variable | Description | Default Value | -| ------------------------ | -------------------------------------------------------------- | ----------------------------------- | -| `PROTOO_LISTEN_PORT` | Port for the protoo WebSocket server and HTTP API server. | `4443` | -| `HTTPS_CERT_FULLCHAIN` | Path to the fullchain certificate file for HTTPS. | `/certs/fullchain.pem` | -| `HTTPS_CERT_PRIVKEY` | Path to the private key file for HTTPS. | `/certs/privkey.pem` | -| `MEDIASOUP_INGRESS_HOST` | Ingress host for the mediasoup client. | | -| `MEDIASOUP_MIN_PORT` | Minimum port for RTC connections in mediasoup. | `40000` | -| `MEDIASOUP_MAX_PORT` | Maximum port for RTC connections in mediasoup. | `49999` | -| `MEDIASOUP_LISTEN_IP` | IP address for mediasoup WebRTC server to listen on. | `0.0.0.0` or `127.0.0.1` | -| `MEDIASOUP_ANNOUNCED_IP` | Public IP address to be announced for mediasoup WebRTC server. | | -| `MEDIASOUP_INGRESS_HOST` | Set Ingress host for /getRoomId response | | +| Variable | Description | Default Value | +| ------------------------ | --------------------------------------------------------- | ----------------------------------- | +| `PROTOO_LISTEN_PORT` | Port for the protoo WebSocket server and HTTP API server. | `4443` | +| `HTTPS_CERT_FULLCHAIN` | Path to the fullchain certificate file for HTTPS. | `/certs/fullchain.pem` | +| `HTTPS_CERT_PRIVKEY` | Path to the private key file for HTTPS. | `/certs/privkey.pem` | +| `MEDIASOUP_INGRESS_HOST` | Ingress host for the mediasoup client. | | +| `MEDIASOUP_MIN_PORT` | Minimum port for RTC connections in mediasoup. | `40000` | +| `MEDIASOUP_MAX_PORT` | Maximum port for RTC connections in mediasoup. | `49999` | +| `MEDIASOUP_LISTEN_IP` | The listening IP for audio/video in mediasoup. | `0.0.0.0` or `127.0.0.1` | +| `MEDIASOUP_ANNOUNCED_IP` | Public IP address for audio/video in mediasoup.. | | +| `MEDIASOUP_INGRESS_HOST` | Set Ingress host for /rooms response | | ## Diagram of solution webrtc-server @@ -88,6 +87,7 @@ services: ``` git clone https://github.com/2060-io/webrtc-server.git +cd package/webrtc-server docker build . -t 2060-webrtc-server:test ``` @@ -117,35 +117,6 @@ docker-compose up ## WebRTC Server API -### GetRoomId - -This endpoint was added to the WebRTC server to generate the roomId and the websocket connection that the mobile application will use for communication. **This endpoint has been Deprecated** - -Set the mandatory variable MEDIASOUP_INGRESS_HOST with the application's ingress to be used by the endpoint to form the connection wsUrl. - -``` -MEDIASOUP_INGRESS_HOST='localhost' -``` - -#### Request - -- Method: GET -- Endpoint: `/getRoomId` -- Port: 443 - -#### Response - -- Status Code: 200 (OK) -- Body: - -```json -{ - "roomId": "8c6ljcfc", - "wsUrl": "wss://localhost:443?roomId=8c6ljcfc&peerId=", - "status": 1 -} -``` - ### Create Rooms This endpoint is used to create a new room or use an existing room based on the provided or generated `roomId`. It returns the WebSocket URL and the `roomId` used, and checks for unique room identifiers before proceeding. @@ -256,14 +227,13 @@ Retrieve ICE Server settings via a peer websocket request to create a WebRTC Tra ```json { - "iceServer": { - "enableIceServer": "yes", - "iceServerHost": "localhost", - "iceServerProto": "udp", - "iceServerPort": "3478", - "iceServerUser": "test", - "iceServerPass": "test123" - } + "iceServers": [ + { + "urls": "turn:localhost:3478?transport=udp", + "username": "test", + "credential": "test123" + } + ] } ``` diff --git a/demo/pymediasoup-client/src/__pycache__/mediasoup.cpython-310.pyc b/demo/pymediasoup-client/src/__pycache__/mediasoup.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..01affc4eaa2dc275f9939b461c37a3e16cd9eef2 GIT binary patch literal 11829 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 u/coverage/', + testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'], + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + isolatedModules: true, + }, + ], + }, + moduleNameMapper: { '^uuid$': 'uuid' }, + testPathIgnorePatterns: ['/dist/'], +} + +export default config diff --git a/packages/loadbalancer/nest-cli.json b/packages/loadbalancer/nest-cli.json new file mode 100644 index 0000000..f9aa683 --- /dev/null +++ b/packages/loadbalancer/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/packages/loadbalancer/package.json b/packages/loadbalancer/package.json new file mode 100644 index 0000000..343975e --- /dev/null +++ b/packages/loadbalancer/package.json @@ -0,0 +1,59 @@ +{ + "name": "webrtc-loadbalancer", + "version": "0.0.1", + "description": "", + "author": "", + "private": true, + "license": "UNLICENSED", + "scripts": { + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json" + }, + "dependencies": { + "@nestjs-modules/ioredis": "^2.0.2", + "@nestjs/axios": "^4.0.0", + "@nestjs/common": "^10.0.0", + "@nestjs/config": "^4.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/swagger": "^8.1.0", + "axios": "^1.7.8", + "class-validator": "^0.14.1", + "ioredis": "^5.4.1", + "reflect-metadata": "^0.1.13", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@nestjs/cli": "^10.0.0", + "@nestjs/schematics": "^10.0.0", + "@nestjs/testing": "^10.0.0", + "@types/express": "^4.17.17", + "@types/jest": "^29.5.2", + "@types/node": "^20.3.1", + "@types/supertest": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.42.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "jest": "^29.5.0", + "prettier": "^3.0.0", + "source-map-support": "^0.5.21", + "supertest": "^6.3.3", + "ts-jest": "^29.1.0", + "ts-loader": "^9.4.3", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.1.3" + } +} diff --git a/src/config/app.config.ts b/packages/loadbalancer/src/config/app.config.ts similarity index 100% rename from src/config/app.config.ts rename to packages/loadbalancer/src/config/app.config.ts diff --git a/src/config/logger.config.ts b/packages/loadbalancer/src/config/logger.config.ts similarity index 100% rename from src/config/logger.config.ts rename to packages/loadbalancer/src/config/logger.config.ts diff --git a/packages/loadbalancer/src/dto/rooms.dto.ts b/packages/loadbalancer/src/dto/rooms.dto.ts new file mode 100644 index 0000000..9838ab0 --- /dev/null +++ b/packages/loadbalancer/src/dto/rooms.dto.ts @@ -0,0 +1,105 @@ +import { ApiProperty } from '@nestjs/swagger' +import { IsString, IsUrl, IsInt, IsUUID, Min, IsOptional } from 'class-validator' + +/** + * DTO for creating a room + */ +export class CreateRoomDto { + @ApiProperty({ + description: 'Event notification URI', + example: 'http://example.com/notification', + required: false, + }) + @IsOptional() + @IsUrl({ require_tld: false }, { message: 'eventNotificationUri must be a valid URL' }) + eventNotificationUri?: string + + @ApiProperty({ + description: 'Maximum number of peers allowed in the room', + example: 3, + required: false, + }) + @IsOptional() + @IsInt({ message: 'maxPeerCount must be an integer' }) + @Min(2, { message: 'maxPeerCount must be at least 2' }) + maxPeerCount?: number +} + +/** + * DTO for registering a server + */ +export class RegisterServerDto { + @ApiProperty({ + description: 'Unique identifier for the server', + example: 'server-12345', + }) + @IsString({ message: 'serverId must be a string' }) + serverId: string + + @ApiProperty({ + description: 'Base URL of the server', + example: 'http://webrtc-service', + }) + @IsUrl({ require_tld: false }, { message: 'url must be a valid URL' }) + url: string + + @ApiProperty({ + description: 'Maximum capacity of the server (number of peers)', + example: 100, + }) + @IsInt({ message: 'capacity must be an integer' }) + @Min(1, { message: 'capacity must be at least 1' }) + capacity: number +} + +/** + * DTO for notifying room closure + */ +export class RoomClosedDto { + @ApiProperty({ + description: 'Unique identifier for the server', + example: 'server-12345', + }) + @IsString({ message: 'serverId must be a string' }) + serverId: string + + @ApiProperty({ + description: 'Unique identifier for the room', + example: 'room-67890', + }) + @IsString({ message: 'roomId must be a string' }) + roomId: string +} + +/** + * Interface for server data + */ +export interface ServerData { + serverId: string + url: string + capacity: number +} + +/** + * Interface for available server + */ +export interface AvailableServer extends ServerData { + load: number +} + +/** + * Interface for room response + */ +export interface RoomResponse { + protocol: string + wsUrl: string + roomId: string +} + +/** + * Interface for room data + */ +export interface RoomData { + serverId: string + roomId: string +} diff --git a/packages/loadbalancer/src/lib/HttpRequestService.ts b/packages/loadbalancer/src/lib/HttpRequestService.ts new file mode 100644 index 0000000..2bc5a1a --- /dev/null +++ b/packages/loadbalancer/src/lib/HttpRequestService.ts @@ -0,0 +1,58 @@ +import { Injectable, Logger } from '@nestjs/common' +import { HttpService } from '@nestjs/axios' +import { firstValueFrom } from 'rxjs' +import { AxiosInstance } from 'axios' +import axios from 'axios' + +@Injectable() +export class HttpRequestService { + private readonly logger = new Logger(HttpRequestService.name) + private readonly httpService: HttpService + + constructor() { + const axiosInstance: AxiosInstance = axios.create() + this.httpService = new HttpService(axiosInstance) + } + + /** + * Sends a POST request to the specified URI with the given data. + * + * @param {string} uri - The URI to which the POST request will be sent. + * @param {Object} data - The payload to send in the POST request body. + */ + public async post(uri: string, data: object): Promise { + if (uri) { + try { + const response = await firstValueFrom(this.httpService.post(uri, data)) + this.logger.log(`[post] Request sent to ${uri}: ${response.status}`) + return response.data + } catch (error) { + this.logger.error(`[post] Failed to send POST request to ${uri}: ${error.message}`) + } + } else { + this.logger.warn(`[post] URI is not defined, cannot send POST request: ${JSON.stringify(data)}`) + } + } + + /** + * Sends a GET request to the specified URI. + * + * @param {string} uri - The URI to which the GET request will be sent. + * @returns {Promise} - The response data from the GET request. + */ + public async get(uri: string): Promise { + if (uri) { + try { + const response = await firstValueFrom(this.httpService.get(uri)) + this.logger.log(`[get] Request sent to ${uri}: ${response.status}`) + return response.data + } catch (error) { + this.logger.error(`[get] Failed to send GET request to ${uri}: ${error.message}`) + throw error + } + } else { + this.logger.warn(`[get] URI is not defined, cannot send GET request`) + return null + } + } +} diff --git a/packages/loadbalancer/src/loadbalancer.controller.spec.ts b/packages/loadbalancer/src/loadbalancer.controller.spec.ts new file mode 100644 index 0000000..2ca7aa0 --- /dev/null +++ b/packages/loadbalancer/src/loadbalancer.controller.spec.ts @@ -0,0 +1,22 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { LoadbalancerController } from './loadbalancer.controller'; +import { LoadbalancerService } from './loadbalancer.service'; + +describe('LoadbalancerController', () => { + let loadbalancerController: LoadbalancerController; + + beforeEach(async () => { + const app: TestingModule = await Test.createTestingModule({ + controllers: [LoadbalancerController], + providers: [LoadbalancerService], + }).compile(); + + loadbalancerController = app.get(LoadbalancerController); + }); + + describe('root', () => { + it('should return "Hello World!"', () => { + expect(loadbalancerController.getHello()).toBe('Hello World!'); + }); + }); +}); diff --git a/packages/loadbalancer/src/loadbalancer.controller.ts b/packages/loadbalancer/src/loadbalancer.controller.ts new file mode 100644 index 0000000..0d72768 --- /dev/null +++ b/packages/loadbalancer/src/loadbalancer.controller.ts @@ -0,0 +1,97 @@ +import { Body, Controller, Get, HttpException, HttpStatus, Logger, Param, Post } from '@nestjs/common' +import { LoadbalancerService } from './loadbalancer.service' +import { ApiBody, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger' +import { CreateRoomDto } from './dto/rooms.dto' + +@Controller() +export class LoadbalancerController { + private readonly logger = new Logger(LoadbalancerController.name) + constructor(private readonly loadbalancerService: LoadbalancerService) {} + + /** + * Endpoint to create or retrieve a room. + * Delegates the logic to the RoomsService. + * @param roomId - The ID of the room (optional). + * @param createRoomDto - Parameters for room creation (optional). + * @returns {object} - Room details and WebSocket URL. + */ + @Post(':roomId?') + @ApiOperation({ summary: 'Create room' }) + @ApiParam({ + name: 'roomId', + description: 'Room identifier (optional)', + required: false, + example: 'room123', + }) + @ApiBody({ + description: 'Parameters for room creation', + schema: { + type: 'object', + properties: { + eventNotificationUri: { + type: 'string', + example: 'http://example.com/notification', + }, + maxPeerCount: { + type: 'number', + example: 3, + }, + }, + }, + }) + @ApiResponse({ + status: 201, + description: 'Room created or retrieved successfully.', + schema: { + example: { + protocol: '2060-mediasoup-v1', + wsUrl: 'wss://example.com:4443', + roomId: '2cc7c7c7', + }, + }, + }) + @ApiResponse({ + status: 500, + description: 'Internal server error.', + }) + async createRoom(@Param('roomId') roomId: string, @Body() createRoomDto: CreateRoomDto) { + const { eventNotificationUri, maxPeerCount } = createRoomDto + try { + return await this.loadbalancerService.createRoom(roomId, eventNotificationUri, maxPeerCount) + } catch (error) { + this.logger.error(`${error.message}`) + throw new HttpException(`${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR) + } + } + + /** + * Endpoint para registrar un servidor en el balanceador de carga. + * @param {object} serverData - Información del servidor. + * @param {string} serverData.serverId - ID único del servidor. + * @param {string} serverData.url - URL base del servidor. + * @param {number} serverData.capacity - Capacidad máxima del servidor. + */ + @Post('register') + async registerServer( + @Body() serverData: { serverId: string; url: string; capacity: number }, + ): Promise<{ message: string }> { + if (!serverData.serverId || !serverData.url || !serverData.capacity) { + throw new HttpException('Invalid server data', HttpStatus.BAD_REQUEST) + } + + await this.loadbalancerService.registerServer(serverData) + return { message: 'Server registered successfully' } + } + + /** + * Endpoint para notificar el cierre de una sala en un servidor. + * @param {object} roomData - Información de la sala. + * @param {string} roomData.serverId - ID del servidor donde se cerró la sala. + * @param {string} roomData.roomId - ID de la sala que se cerró. + */ + @Post('room-closed') + async notifyRoomClosed(@Body() roomData: { serverId: string; roomId: string }): Promise<{ message: string }> { + await this.loadbalancerService.roomClosed(roomData) + return { message: 'Room closed notification processed successfully' } + } +} diff --git a/packages/loadbalancer/src/loadbalancer.module.ts b/packages/loadbalancer/src/loadbalancer.module.ts new file mode 100644 index 0000000..3e0ff11 --- /dev/null +++ b/packages/loadbalancer/src/loadbalancer.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common' +import { LoadbalancerController } from './loadbalancer.controller' +import { LoadbalancerService } from './loadbalancer.service' +import { ConfigModule } from '@nestjs/config' +import appConfig from './config/app.config' +import { HandledRedisModule } from './modules/redis.module' +import { HttpRequestService } from './lib/HttpRequestService' + +@Module({ + imports: [ + ConfigModule.forRoot({ + load: [appConfig], + isGlobal: true, + }), + HandledRedisModule, + ], + controllers: [LoadbalancerController], + providers: [LoadbalancerService, HttpRequestService], +}) +export class LoadbalancerModule {} diff --git a/packages/loadbalancer/src/loadbalancer.service.ts b/packages/loadbalancer/src/loadbalancer.service.ts new file mode 100644 index 0000000..b27c8f1 --- /dev/null +++ b/packages/loadbalancer/src/loadbalancer.service.ts @@ -0,0 +1,179 @@ +import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common' +import { HttpRequestService } from './lib/HttpRequestService' +import { ApiResponse } from '@nestjs/swagger' +import Redis from 'ioredis' +import { InjectRedis } from '@nestjs-modules/ioredis' +import { AvailableServer, RoomData, RoomResponse } from './dto/rooms.dto' + +@Injectable() +export class LoadbalancerService { + private readonly logger = new Logger(LoadbalancerService.name) + private readonly redisClient: Redis + + constructor( + private readonly httpRequestService: HttpRequestService, + @InjectRedis() private readonly redis: Redis, + ) { + this.redisClient = this.redis.duplicate() + } + /** + * Create or retrieve a room. + * Handles the logic for creating or retrieving a room based on the `roomId`. + * + * @param {string} [roomId] - The ID of the room (optional). + * @param {string} [eventNotificationUri] - The URI for event notifications (optional). + * @param {number} [maxPeerCount] - The maximum number of peers allowed (optional). + * @returns {Promise} - Room details and WebSocket URL. + * @throws {HttpException} - Throws if no servers are available or if the room creation fails. + */ + public async createRoom( + roomId?: string, + eventNotificationUri?: string, + maxPeerCount?: number, + ): Promise { + try { + this.logger.log('[createRoom] Attempting to create or retrieve a room.') + + // Retrieve the best server based on load + const bestServer = await this.getBestServer() + if (!bestServer) { + this.logger.error('[createRoom] No servers available to handle the request.') + throw new Error('[createRoom] No servers available') + } + + this.logger.log(`[createRoom] Selected best server: ${JSON.stringify(bestServer)}`) + + // Build the room creation payload + const roomCreationData = { + eventNotificationUri, + maxPeerCount, + } + + // Send a POST request to the selected server to create the room + const response = await this.httpRequestService.post(`${bestServer.url}/rooms/${roomId}`, roomCreationData) + + this.logger.log( + `[createRoom] Room created successfully on server ${bestServer.url}. Response: ${JSON.stringify(response)}`, + ) + // increment incrementRoomCount + this.incrementRoomCount(bestServer.serverId) + // Return the response containing room details + return response + } catch (error) { + this.logger.error(`[createRoom] Error creating or retrieving room: ${error.message}`) + throw new HttpException({ error: error.message }, HttpStatus.INTERNAL_SERVER_ERROR) + } + } + + /** + * Registers a server in Redis. + * This method stores server information in Redis for load balancing. + * + * @param {object} serverData - The server data to register. + * @param {string} serverData.serverId - Unique identifier for the server. + * @param {string} serverData.url - Base URL of the server. + * @param {number} serverData.capacity - Maximum capacity of the server. + * @returns {Promise} - Resolves when the server is successfully registered. + */ + public async registerServer(serverData: { serverId: string; url: string; capacity: number }): Promise { + try { + const key = `server:${serverData.serverId}` + await this.redisClient.hset(key, 'url', serverData.url, 'capacity', serverData.capacity.toString(), 'load', '0') + this.logger.log(`[registerServer] Server registered successfully: ${JSON.stringify(serverData)}`) + } catch (error) { + this.logger.error(`[registerServer] Failed to register server: ${error.message}`) + throw new HttpException({ error: error.message }, HttpStatus.INTERNAL_SERVER_ERROR) + } + } + + /** + * Increments the number of active rooms for a server. + * @param {string} serverId - The ID of the server. + */ + public async incrementRoomCount(serverId: string): Promise { + const key = `server:${serverId}` + const exists = await this.redisClient.exists(key) + if (!exists) { + this.logger.warn(`[incrementRoomCount] Server ${serverId} not found in Redis.`) + return + } + await this.redisClient.hincrby(key, 'rooms', 1) + this.logger.log(`[incrementRoomCount] Incremented room count for server ${serverId}.`) + } + + /** + * Decrements the number of active rooms for a server. + * @param {RoomData} roomData - The data of the room that was closed. + */ + public async roomClosed(roomData: RoomData): Promise { + const key = `server:${roomData.serverId}` + const exists = await this.redisClient.exists(key) + if (!exists) { + this.logger.warn(`[roomClosed] Server ${roomData.serverId} not found in Redis.`) + return + } + + const currentRooms = await this.redisClient.hget(key, 'rooms') + if (Number(currentRooms) > 0) { + await this.redisClient.hincrby(key, 'rooms', -1) + this.logger.log( + `[roomClosed] Decremented room count for server ${roomData.serverId}. Room ID: ${roomData.roomId}`, + ) + } else { + this.logger.warn(`[roomClosed] Room count for server ${roomData.serverId} is already 0.`) + } + } + + /** + * Retrieves the list of available servers sorted by load. + * + * @returns {Promise} - List of servers with their load and capacity. + * @throws {HttpException} - Throws if Redis fails to retrieve server information. + */ + public async getAvailableServers(): Promise { + try { + this.logger.log('[getAvailableServers] Fetching available servers from Redis.') + const keys = await this.redisClient.keys('server:*') + const servers = [] + + for (const key of keys) { + const serverData = await this.redisClient.hgetall(key) + servers.push({ + serverId: key.split(':')[1], + ...serverData, + capacity: Number(serverData.capacity), + load: Number(serverData.load), + }) + } + + // Sort servers by load (load/capacity ratio) + return servers.sort((a, b) => a.load / a.capacity - b.load / b.capacity) + } catch (error) { + this.logger.error(`[getAvailableServers] Failed to retrieve available servers: ${error.message}`) + throw new HttpException({ error: error.message }, HttpStatus.INTERNAL_SERVER_ERROR) + } + } + + /** + * Retrieves the server with the least load. + * + * @returns {Promise} - The server with the least load. + * @throws {HttpException} - Throws if no servers are available. + */ + public async getBestServer(): Promise { + try { + const servers = await this.getAvailableServers() + if (servers.length === 0) { + this.logger.warn('[getBestServer] No servers available.') + throw new Error('[getBestServer] No servers available') + } + + const bestServer = servers[0] + this.logger.log(`[getBestServer] Best server selected: ${JSON.stringify(bestServer)}`) + return bestServer + } catch (error) { + this.logger.error(`[getBestServer] Error selecting the best server: ${error.message}`) + throw new HttpException({ error: error.message }, HttpStatus.INTERNAL_SERVER_ERROR) + } + } +} diff --git a/packages/loadbalancer/src/main.ts b/packages/loadbalancer/src/main.ts new file mode 100644 index 0000000..6a556b6 --- /dev/null +++ b/packages/loadbalancer/src/main.ts @@ -0,0 +1,56 @@ +import { NestFactory } from '@nestjs/core' +import { LoadbalancerModule } from './loadbalancer.module' +import { Logger, ValidationPipe, VersioningType } from '@nestjs/common' +import { getLogLevels } from './config/logger.config' +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger' +import { ConfigService } from '@nestjs/config' +import * as express from 'express' +import * as fs from 'fs' + +async function bootstrap() { + const logger = new Logger('Bootstrap') + + // Retrieve log levels based on environment configuration + const logLevels = getLogLevels() + + const app = await NestFactory.create(LoadbalancerModule, { + logger: logLevels, + }) + + const expressApp = app.getHttpAdapter().getInstance() as express.Application + expressApp.set('case sensitive routing', true) + + app.enableVersioning({ type: VersioningType.URI }) + app.enableCors() + + // Validations DTO + app.useGlobalPipes( + new ValidationPipe({ + transform: true, + whitelist: true, + forbidNonWhitelisted: true, + disableErrorMessages: false, + }), + ) + + // Documentation Builder + const config = new DocumentBuilder() + .setTitle('Webrtc Server Loadbalancer') + .setDescription('API to WebRtc Server Loadbalancer') + .setVersion('1.1') + .addTag('rooms') + .build() + const document = SwaggerModule.createDocument(app, config) + SwaggerModule.setup('API', app, document) + + const configService = app.get(ConfigService) + + const PORT = configService.get('appConfig.port') + + // Start the server + app.listen(PORT, () => { + const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8')) + logger.log(`Application (${packageJson.name} v${packageJson.version}) running on: https://localhost:${PORT}`) + }) +} +bootstrap() diff --git a/src/modules/redis.module.ts b/packages/loadbalancer/src/modules/redis.module.ts similarity index 100% rename from src/modules/redis.module.ts rename to packages/loadbalancer/src/modules/redis.module.ts diff --git a/packages/loadbalancer/test/app.e2e-spec.ts b/packages/loadbalancer/test/app.e2e-spec.ts new file mode 100644 index 0000000..50cda62 --- /dev/null +++ b/packages/loadbalancer/test/app.e2e-spec.ts @@ -0,0 +1,24 @@ +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('/ (GET)', () => { + return request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!'); + }); +}); diff --git a/packages/loadbalancer/test/jest-e2e.json b/packages/loadbalancer/test/jest-e2e.json new file mode 100644 index 0000000..e9d912f --- /dev/null +++ b/packages/loadbalancer/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/packages/loadbalancer/tsconfig.app.json b/packages/loadbalancer/tsconfig.app.json new file mode 100644 index 0000000..2c2ecbb --- /dev/null +++ b/packages/loadbalancer/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/packages/loadbalancer" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/packages/loadbalancer/tsconfig.build.json b/packages/loadbalancer/tsconfig.build.json new file mode 100644 index 0000000..a5c00f4 --- /dev/null +++ b/packages/loadbalancer/tsconfig.build.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "module": "commonjs", + "types": ["node"], + "target": "ES2021", + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false, + "useUnknownInCatchVariables": false, + "declaration": true, + "sourceMap": true, + "strict": true, + "noEmitOnError": true, + "lib": [], + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/packages/loadbalancer/tsconfig.json b/packages/loadbalancer/tsconfig.json new file mode 100644 index 0000000..78184e6 --- /dev/null +++ b/packages/loadbalancer/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.build.json", + "ts-node": { + "require": ["tsconfig-paths/register"], + "files": true + }, + "compilerOptions": { + "baseUrl": ".", + "types": ["node", "jest"] + }, + "exclude": ["node_modules", "dist"] +} diff --git a/Dockerfile b/packages/webrtc-server/Dockerfile similarity index 100% rename from Dockerfile rename to packages/webrtc-server/Dockerfile diff --git a/docker-compose-lb.yml b/packages/webrtc-server/docker-compose-lb.yml similarity index 100% rename from docker-compose-lb.yml rename to packages/webrtc-server/docker-compose-lb.yml diff --git a/docker-compose.yml b/packages/webrtc-server/docker-compose.yml similarity index 89% rename from docker-compose.yml rename to packages/webrtc-server/docker-compose.yml index fef572a..705e59d 100644 --- a/docker-compose.yml +++ b/packages/webrtc-server/docker-compose.yml @@ -5,14 +5,15 @@ services: image: webrtc-server:test environment: - LOG_LEVEL=3 - - MEDIASOUP_ANNOUNCED_IP=192.168.10.15 + - MEDIASOUP_ANNOUNCED_IP=192.168.100.84 + - MEDIASOUP_LISTEN_IP=192.168.100.84 - PROTOO_LISTEN_PORT=4443 - MEDIASOUP_MIN_PORT=40000 - MEDIASOUP_MAX_PORT=50000 - HTTPS_CERT_FULLCHAIN=/config/certs/fullchain.pem - HTTPS_CERT_PRIVKEY=/config/certs/privkey.pem - MEDIASOUP_CLIENT_ENABLE_ICESERVER=yes - - MEDIASOUP_CLIENT_ICESERVER_HOST=192.168.10.15 + - MEDIASOUP_CLIENT_ICESERVER_HOST=192.168.100.84 - MEDIASOUP_CLIENT_ICESERVER_PROTO=udp - MEDIASOUP_CLIENT_ICESERVER_PORT=3478 - MEDIASOUP_CLIENT_ICESERVER_USER=test diff --git a/packages/webrtc-server/jest.config.ts b/packages/webrtc-server/jest.config.ts new file mode 100644 index 0000000..e9d8280 --- /dev/null +++ b/packages/webrtc-server/jest.config.ts @@ -0,0 +1,21 @@ +import type { Config } from '@jest/types' + +const config: Config.InitialOptions = { + preset: 'ts-jest', + testEnvironment: 'node', + coveragePathIgnorePatterns: ['/build/', '/node_modules/', '/__tests__/', 'tests'], + coverageDirectory: '/coverage/', + testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'], + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + isolatedModules: true, + }, + ], + }, + moduleNameMapper: { '^uuid$': 'uuid' }, + testPathIgnorePatterns: ['/dist/'], +} + +export default config diff --git a/packages/webrtc-server/nest-cli.json b/packages/webrtc-server/nest-cli.json new file mode 100644 index 0000000..97c557e --- /dev/null +++ b/packages/webrtc-server/nest-cli.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true, + "tsConfigPath": "tsconfig.json" + } +} diff --git a/packages/webrtc-server/package.json b/packages/webrtc-server/package.json new file mode 100644 index 0000000..935d4e8 --- /dev/null +++ b/packages/webrtc-server/package.json @@ -0,0 +1,73 @@ +{ + "name": "webrtc-server", + "version": "0.0.1", + "description": "", + "author": "", + "private": true, + "license": "Apache-2.0", + "scripts": { + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\"", + "check-types": "eslint \"{src,packages,libs,test}/**/*.ts\" --ignore-pattern demo/", + "start": "nest start", + "start:dev": "LOG_LEVEL=3 nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/packages/webrtc-server/main", + "lint": "eslint \"{src,test}/**/*.ts\" --fix", + "test": "jest --config jest.config.ts", + "test:watch": "jest --watch --config jest.config.ts", + "test:cov": "jest --coverage --config jest.config.ts", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand --config jest.config.ts", + "test:e2e": "jest --config ./test/jest-e2e.json" + }, + "dependencies": { + "@nestjs-modules/ioredis": "^2.0.2", + "@nestjs/axios": "^3.1.2", + "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.3.0", + "@nestjs/core": "^10.0.0", + "@nestjs/mapped-types": "*", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/swagger": "^8.1.0", + "@sitespeed.io/throttle": "^3.1.1", + "axios": "^1.7.8", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "inquirer": "^12.1.0", + "ioredis": "^5.4.1", + "mediasoup": "^3.15.2", + "protoo-client": "^4.0.6", + "protoo-server": "^4.0.6", + "reflect-metadata": "^0.1.13", + "rxjs": "^7.8.1", + "string-width": "4", + "uuid": "^11.0.3", + "wrap-ansi": "5" + }, + "devDependencies": { + "@nestjs/cli": "^10.4.8", + "@nestjs/schematics": "^10.0.0", + "@nestjs/testing": "^10.4.15", + "@types/babel__core": "^7.20.5", + "@types/express": "^4.17.17", + "@types/jest": "^29.5.14", + "@types/node": "^22.10.1", + "@types/protoo-server": "^4.0.6", + "@types/supertest": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.42.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "jest": "^29.7.0", + "prettier": "^3.0.0", + "selfsigned": "^2.4.1", + "source-map-support": "^0.5.21", + "supertest": "^6.3.3", + "ts-jest": "^29.1.0", + "ts-loader": "^9.4.3", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.1.3" + } +} diff --git a/src/app.module.ts b/packages/webrtc-server/src/app.module.ts similarity index 100% rename from src/app.module.ts rename to packages/webrtc-server/src/app.module.ts diff --git a/packages/webrtc-server/src/config/app.config.ts b/packages/webrtc-server/src/config/app.config.ts new file mode 100644 index 0000000..4a52b0c --- /dev/null +++ b/packages/webrtc-server/src/config/app.config.ts @@ -0,0 +1,43 @@ +import { registerAs } from '@nestjs/config' + +/** + * Configuration for the application, including ports, database URIs, and service URLs. + * + * @returns {object} - An object containing the configuration settings for the application. + */ +export default registerAs('appConfig', () => ({ + /** + * The port number on which the application will run. + * Defaults to 3500 if APP_PORT is not set in the environment variables. + * @type {number} + */ + appPort: parseInt(process.env.APP_PORT, 10) || 3001, + + /** + * Defines the Redis mode, which can be 'single' or 'cluster'. + * Defaults to 'single' if REDIS_TYPE is not set in the environment variables. + * @type {string} + */ + redisType: process.env.REDIS_TYPE || 'single', + + /** + * The Redis database URL for connecting to the Redis server Single Mode. + * Defaults to a specified local Redis instance if REDIS_URL is not set in the environment variables. + * @type {string} + */ + redisDbUrl: process.env.REDIS_URL || 'redis://localhost:6379', + + /** + * A comma-separated list of Redis nodes in 'host:port' format, used in cluster mode. + * Only relevant if REDIS_TYPE is set to 'cluster'. + * @type {string | undefined} + */ + redisNodes: process.env.REDIS_NODES, + + /** + * The NAT mapping for Redis nodes, defined in 'externalAddress:host:port' format. + * Useful for Redis cluster configurations with external IP mappings. + * @type {string | undefined} + */ + redisNatmap: process.env.REDIS_NATMAP, +})) diff --git a/src/config/config.server.ts b/packages/webrtc-server/src/config/config.server.ts similarity index 100% rename from src/config/config.server.ts rename to packages/webrtc-server/src/config/config.server.ts diff --git a/src/config/constants.ts b/packages/webrtc-server/src/config/constants.ts similarity index 100% rename from src/config/constants.ts rename to packages/webrtc-server/src/config/constants.ts diff --git a/packages/webrtc-server/src/config/logger.config.ts b/packages/webrtc-server/src/config/logger.config.ts new file mode 100644 index 0000000..6da20ab --- /dev/null +++ b/packages/webrtc-server/src/config/logger.config.ts @@ -0,0 +1,26 @@ +import { LogLevel, Logger } from '@nestjs/common' + +export function getLogLevels(): LogLevel[] { + const logger = new Logger('getLogLevels') + const logLevelConfig = parseInt(process.env.LOG_LEVEL, 10) || 1 + + const logLevels: LogLevel[] = ['log'] // Default to 'log' + + // Adjust log levels based on the configuration + switch (logLevelConfig) { + case 2: + logLevels.push('debug') + logger.log('Log levels set to: log, debug') + break + case 3: + logLevels.push('debug', 'error') + logger.log('Log levels set to: log, debug, error') + break + default: + logLevels.push('error') // Default to 'log' and 'error' + logger.log('Log levels set to: log, error') + } + + logger.log(`Configured log level: ${logLevels}`) + return logLevels +} diff --git a/src/lib/Room.ts b/packages/webrtc-server/src/lib/Room.ts similarity index 99% rename from src/lib/Room.ts rename to packages/webrtc-server/src/lib/Room.ts index 6fefabc..6f7f965 100644 --- a/src/lib/Room.ts +++ b/packages/webrtc-server/src/lib/Room.ts @@ -1197,7 +1197,7 @@ export class Room extends EventEmitter { // Must take the Transport the remote Peer is using for consuming. const transport = Array.from(consumerPeer.data.transports.values()).find( - (t: mediasoup.types.Transport) => t.appData.consuming, + (t) => (t as mediasoup.types.Transport).appData.consuming, ) as mediasoup.types.Transport // This should not happen. diff --git a/src/lib/RoomFactory.ts b/packages/webrtc-server/src/lib/RoomFactory.ts similarity index 100% rename from src/lib/RoomFactory.ts rename to packages/webrtc-server/src/lib/RoomFactory.ts diff --git a/src/lib/notification.service.ts b/packages/webrtc-server/src/lib/notification.service.ts similarity index 100% rename from src/lib/notification.service.ts rename to packages/webrtc-server/src/lib/notification.service.ts diff --git a/src/lib/room.interfaces.ts b/packages/webrtc-server/src/lib/room.interfaces.ts similarity index 100% rename from src/lib/room.interfaces.ts rename to packages/webrtc-server/src/lib/room.interfaces.ts diff --git a/src/main.ts b/packages/webrtc-server/src/main.ts similarity index 98% rename from src/main.ts rename to packages/webrtc-server/src/main.ts index b7cb5a6..daf8d94 100644 --- a/src/main.ts +++ b/packages/webrtc-server/src/main.ts @@ -4,7 +4,7 @@ import { Logger, ValidationPipe, VersioningType } from '@nestjs/common' import { createServer } from 'https' import { ExpressAdapter } from '@nestjs/platform-express' import { getLogLevels } from './config/logger.config' -import * as express from 'express' +import express from 'express' import * as fs from 'fs' import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger' import { config as configServer } from './config/config.server' diff --git a/packages/webrtc-server/src/modules/redis.module.ts b/packages/webrtc-server/src/modules/redis.module.ts new file mode 100644 index 0000000..b175de3 --- /dev/null +++ b/packages/webrtc-server/src/modules/redis.module.ts @@ -0,0 +1,79 @@ +import { Module, Logger } from '@nestjs/common' +import { RedisModule, RedisModuleOptions } from '@nestjs-modules/ioredis' +import { ConfigModule, ConfigService } from '@nestjs/config' + +@Module({ + imports: [ + RedisModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService): RedisModuleOptions => { + const logger = new Logger('RedisModule') + const redisType = configService.get('appConfig.redisType', 'single') + logger.log(`[RedisModule] Configuring Redis with type: ${redisType}`) + if (redisType === 'cluster') { + const nodes = configService + .get('appConfig.redisNodes', '') + .split(',') + .map((node) => { + const [host, port] = node.split(':') + return { host, port: parseInt(port, 10) } + }) + logger.debug(`[RedisModule] Cluster Nodes: ${nodes}`) + const natMap = configService + .get('appConfig.redisNatmap', '') + .split(',') + .reduce( + (map, entry) => { + const [externalAddress, externalPort, internalHost, internalPort] = entry.split(':') + map[`${externalAddress}:${externalPort}`] = { host: internalHost, port: parseInt(internalPort, 10) } + return map + }, + {} as Record, + ) + logger.debug(`[RedisModule] Cluster Nat: ${nodes}`) + return { + type: 'cluster', + nodes, + options: { + natMap, + redisOptions: { + connectTimeout: 10000, // Maximum wait time for connection in milliseconds + maxRetriesPerRequest: 5, // Maximum number of retries per request + enableReadyCheck: true, // Ensure the cluster is ready before processing commands + reconnectOnError(err: Error): boolean { + const targetError = 'READONLY' + if (err.message.includes(targetError)) { + console.error('Reconnect due to READONLY error:', err) + return true + } + return false + }, + }, + }, + } + } else { + return { + type: 'single', + url: configService.get('REDIS_URL', 'redis://localhost:6379'), + options: { + connectTimeout: 10000, // Maximum wait time for connection in milliseconds + maxRetriesPerRequest: 5, // Maximum number of retries per request + enableReadyCheck: true, // Ensure the instance is ready before processing commands + reconnectOnError(err: Error): boolean { + const targetError = 'READONLY' + if (err.message.includes(targetError)) { + console.error('Reconnect due to READONLY error:', err) + return true + } + return false + }, + }, + } + } + }, + }), + ], + exports: [RedisModule], +}) +export class HandledRedisModule {} diff --git a/src/rooms/dto/rooms.dto.ts b/packages/webrtc-server/src/rooms/dto/rooms.dto.ts similarity index 100% rename from src/rooms/dto/rooms.dto.ts rename to packages/webrtc-server/src/rooms/dto/rooms.dto.ts diff --git a/src/rooms/rooms.controller.spec.ts b/packages/webrtc-server/src/rooms/rooms.controller.spec.ts similarity index 100% rename from src/rooms/rooms.controller.spec.ts rename to packages/webrtc-server/src/rooms/rooms.controller.spec.ts diff --git a/src/rooms/rooms.controller.ts b/packages/webrtc-server/src/rooms/rooms.controller.ts similarity index 100% rename from src/rooms/rooms.controller.ts rename to packages/webrtc-server/src/rooms/rooms.controller.ts diff --git a/src/rooms/rooms.module.ts b/packages/webrtc-server/src/rooms/rooms.module.ts similarity index 100% rename from src/rooms/rooms.module.ts rename to packages/webrtc-server/src/rooms/rooms.module.ts diff --git a/src/rooms/rooms.service.spec.ts b/packages/webrtc-server/src/rooms/rooms.service.spec.ts similarity index 99% rename from src/rooms/rooms.service.spec.ts rename to packages/webrtc-server/src/rooms/rooms.service.spec.ts index 6bbb189..cc32bde 100644 --- a/src/rooms/rooms.service.spec.ts +++ b/packages/webrtc-server/src/rooms/rooms.service.spec.ts @@ -1,4 +1,4 @@ -import * as fs from 'fs' +import fs from 'fs' import { RoomsService } from './rooms.service' import { Room } from '../lib/Room' import * as mediasoup from 'mediasoup' @@ -6,7 +6,7 @@ import * as protoo from 'protoo-server' import { Test } from '@nestjs/testing' import { ConfigService } from '@nestjs/config' import { HttpException, HttpStatus } from '@nestjs/common' -import * as url from 'url' +import url from 'url' jest.mock('../lib/Room', () => ({ Room: { diff --git a/src/rooms/rooms.service.ts b/packages/webrtc-server/src/rooms/rooms.service.ts similarity index 100% rename from src/rooms/rooms.service.ts rename to packages/webrtc-server/src/rooms/rooms.service.ts diff --git a/test/app.e2e-spec.ts b/packages/webrtc-server/test/app.e2e-spec.ts similarity index 97% rename from test/app.e2e-spec.ts rename to packages/webrtc-server/test/app.e2e-spec.ts index d7e1557..6c0c5bc 100644 --- a/test/app.e2e-spec.ts +++ b/packages/webrtc-server/test/app.e2e-spec.ts @@ -1,10 +1,10 @@ import { createServer, Server } from 'https' import { ConsoleLogger, INestApplication, ValidationPipe } from '@nestjs/common' import { Test } from '@nestjs/testing' -import * as express from 'express' +import express from 'express' import { ExpressAdapter } from '@nestjs/platform-express' import { AppModule } from '../src/app.module' -import * as request from 'supertest' +import request from 'supertest' import * as selfsigned from 'selfsigned' jest.setTimeout(20000) diff --git a/test/jest-e2e.json b/packages/webrtc-server/test/jest-e2e.json similarity index 100% rename from test/jest-e2e.json rename to packages/webrtc-server/test/jest-e2e.json diff --git a/packages/webrtc-server/tsconfig.app.json b/packages/webrtc-server/tsconfig.app.json new file mode 100644 index 0000000..5f003fb --- /dev/null +++ b/packages/webrtc-server/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/packages/webrtc-server" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/packages/webrtc-server/tsconfig.build.json b/packages/webrtc-server/tsconfig.build.json new file mode 100644 index 0000000..a5c00f4 --- /dev/null +++ b/packages/webrtc-server/tsconfig.build.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "module": "commonjs", + "types": ["node"], + "target": "ES2021", + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false, + "useUnknownInCatchVariables": false, + "declaration": true, + "sourceMap": true, + "strict": true, + "noEmitOnError": true, + "lib": [], + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/packages/webrtc-server/tsconfig.json b/packages/webrtc-server/tsconfig.json new file mode 100644 index 0000000..78184e6 --- /dev/null +++ b/packages/webrtc-server/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.build.json", + "ts-node": { + "require": ["tsconfig-paths/register"], + "files": true + }, + "compilerOptions": { + "baseUrl": ".", + "types": ["node", "jest"] + }, + "exclude": ["node_modules", "dist"] +} diff --git a/packages/webrtc-server/yarn.lock b/packages/webrtc-server/yarn.lock new file mode 100644 index 0000000..9724b0c --- /dev/null +++ b/packages/webrtc-server/yarn.lock @@ -0,0 +1,5699 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.2.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@angular-devkit/core@17.3.11": + version "17.3.11" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-17.3.11.tgz#a74b042ec06cf626d5a2f6a3971b156c6759fe09" + integrity sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ== + 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" + +"@angular-devkit/schematics-cli@17.3.11": + version "17.3.11" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics-cli/-/schematics-cli-17.3.11.tgz#dd1963d592ae7d2555ddbbac8406ba03bcddf4fe" + integrity sha512-kcOMqp+PHAKkqRad7Zd7PbpqJ0LqLaNZdY1+k66lLWmkEBozgq8v4ASn/puPWf9Bo0HpCiK+EzLf0VHE8Z/y6Q== + 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" + +"@angular-devkit/schematics@17.3.11": + version "17.3.11" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-17.3.11.tgz#37095fb08b0ab0343c7c0dde57ca81115178714f" + integrity sha512-I5wviiIqiFwar9Pdk30Lujk8FczEEc18i22A5c6Z9lbmhPQdTroDnEQdsfXjy404wPe8H62s0I15o4pmMGfTYQ== + 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" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.2": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" + integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/compat-data@^7.26.5": + version "7.26.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.5.tgz#df93ac37f4417854130e21d72c66ff3d4b897fc7" + integrity sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg== + +"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.23.9": + version "7.26.7" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.7.tgz#0439347a183b97534d52811144d763a17f9d2b24" + integrity sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.26.2" + "@babel/generator" "^7.26.5" + "@babel/helper-compilation-targets" "^7.26.5" + "@babel/helper-module-transforms" "^7.26.0" + "@babel/helpers" "^7.26.7" + "@babel/parser" "^7.26.7" + "@babel/template" "^7.25.9" + "@babel/traverse" "^7.26.7" + "@babel/types" "^7.26.7" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.26.5", "@babel/generator@^7.7.2": + version "7.26.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.5.tgz#e44d4ab3176bbcaf78a5725da5f1dc28802a9458" + integrity sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw== + dependencies: + "@babel/parser" "^7.26.5" + "@babel/types" "^7.26.5" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^3.0.2" + +"@babel/helper-compilation-targets@^7.26.5": + version "7.26.5" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz#75d92bb8d8d51301c0d49e52a65c9a7fe94514d8" + integrity sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA== + dependencies: + "@babel/compat-data" "^7.26.5" + "@babel/helper-validator-option" "^7.25.9" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-module-imports@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715" + integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw== + dependencies: + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/helper-module-transforms@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz#8ce54ec9d592695e58d84cd884b7b5c6a2fdeeae" + integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw== + dependencies: + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.25.9", "@babel/helper-plugin-utils@^7.8.0": + version "7.26.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz#18580d00c9934117ad719392c4f6585c9333cc35" + integrity sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg== + +"@babel/helper-string-parser@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + +"@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + +"@babel/helper-validator-option@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72" + integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw== + +"@babel/helpers@^7.26.7": + version "7.26.7" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.7.tgz#fd1d2a7c431b6e39290277aacfd8367857c576a4" + integrity sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A== + dependencies: + "@babel/template" "^7.25.9" + "@babel/types" "^7.26.7" + +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.25.9", "@babel/parser@^7.26.5", "@babel/parser@^7.26.7": + version "7.26.7" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.7.tgz#e114cd099e5f7d17b05368678da0fb9f69b3385c" + integrity sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w== + dependencies: + "@babel/types" "^7.26.7" + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-bigint@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-import-attributes@^7.24.7": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz#3b1412847699eea739b4f2602c74ce36f6b0b0f7" + integrity sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-syntax-import-meta@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@^7.7.2": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz#a34313a178ea56f1951599b929c1ceacee719290" + integrity sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-typescript@^7.7.2": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz#67dda2b74da43727cf21d46cf9afef23f4365399" + integrity sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/template@^7.25.9", "@babel/template@^7.3.3": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016" + integrity sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg== + dependencies: + "@babel/code-frame" "^7.25.9" + "@babel/parser" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/traverse@^7.25.9", "@babel/traverse@^7.26.7": + version "7.26.7" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.7.tgz#99a0a136f6a75e7fb8b0a1ace421e0b25994b8bb" + integrity sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA== + dependencies: + "@babel/code-frame" "^7.26.2" + "@babel/generator" "^7.26.5" + "@babel/parser" "^7.26.7" + "@babel/template" "^7.25.9" + "@babel/types" "^7.26.7" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.25.9", "@babel/types@^7.26.5", "@babel/types@^7.26.7", "@babel/types@^7.3.3": + version "7.26.7" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.7.tgz#5e2b89c0768e874d4d061961f3a5a153d71dc17a" + integrity sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz#d1145bf2c20132d6400495d6df4bf59362fd9d56" + integrity sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA== + dependencies: + eslint-visitor-keys "^3.4.3" + +"@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1": + version "4.12.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" + integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== + +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.6.0" + globals "^13.19.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@8.57.1": + version "8.57.1" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" + integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== + +"@humanwhocodes/config-array@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" + integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== + dependencies: + "@humanwhocodes/object-schema" "^2.0.3" + debug "^4.3.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== + +"@inquirer/checkbox@^4.0.6": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@inquirer/checkbox/-/checkbox-4.0.6.tgz#e71401a7e1900332f17ed68c172a89fe20225f49" + integrity sha512-PgP35JfmGjHU0LSXOyRew0zHuA9N6OJwOlos1fZ20b7j8ISeAdib3L+n0jIxBtX958UeEpte6xhG/gxJ5iUqMw== + dependencies: + "@inquirer/core" "^10.1.4" + "@inquirer/figures" "^1.0.9" + "@inquirer/type" "^3.0.2" + ansi-escapes "^4.3.2" + yoctocolors-cjs "^2.1.2" + +"@inquirer/confirm@^5.1.3": + version "5.1.3" + resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-5.1.3.tgz#c1ad57663f54758981811ccb86f823072ddf5c1a" + integrity sha512-fuF9laMmHoOgWapF9h9hv6opA5WvmGFHsTYGCmuFxcghIhEhb3dN0CdQR4BUMqa2H506NCj8cGX4jwMsE4t6dA== + dependencies: + "@inquirer/core" "^10.1.4" + "@inquirer/type" "^3.0.2" + +"@inquirer/core@^10.1.4": + version "10.1.4" + resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-10.1.4.tgz#02394e68d894021935caca0d10fc68fd4f3a3ead" + integrity sha512-5y4/PUJVnRb4bwWY67KLdebWOhOc7xj5IP2J80oWXa64mVag24rwQ1VAdnj7/eDY/odhguW0zQ1Mp1pj6fO/2w== + dependencies: + "@inquirer/figures" "^1.0.9" + "@inquirer/type" "^3.0.2" + ansi-escapes "^4.3.2" + cli-width "^4.1.0" + mute-stream "^2.0.0" + signal-exit "^4.1.0" + strip-ansi "^6.0.1" + wrap-ansi "^6.2.0" + yoctocolors-cjs "^2.1.2" + +"@inquirer/editor@^4.2.3": + version "4.2.3" + resolved "https://registry.yarnpkg.com/@inquirer/editor/-/editor-4.2.3.tgz#0858adcd07d9607b0614778eaa5ce8a83871c367" + integrity sha512-S9KnIOJuTZpb9upeRSBBhoDZv7aSV3pG9TECrBj0f+ZsFwccz886hzKBrChGrXMJwd4NKY+pOA9Vy72uqnd6Eg== + dependencies: + "@inquirer/core" "^10.1.4" + "@inquirer/type" "^3.0.2" + external-editor "^3.1.0" + +"@inquirer/expand@^4.0.6": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@inquirer/expand/-/expand-4.0.6.tgz#8676e6049c6114fb306df23358375bd84fa1c92c" + integrity sha512-TRTfi1mv1GeIZGyi9PQmvAaH65ZlG4/FACq6wSzs7Vvf1z5dnNWsAAXBjWMHt76l+1hUY8teIqJFrWBk5N6gsg== + dependencies: + "@inquirer/core" "^10.1.4" + "@inquirer/type" "^3.0.2" + yoctocolors-cjs "^2.1.2" + +"@inquirer/figures@^1.0.9": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@inquirer/figures/-/figures-1.0.9.tgz#9d8128f8274cde4ca009ca8547337cab3f37a4a3" + integrity sha512-BXvGj0ehzrngHTPTDqUoDT3NXL8U0RxUk2zJm2A66RhCEIWdtU1v6GuUqNAgArW4PQ9CinqIWyHdQgdwOj06zQ== + +"@inquirer/input@^4.1.3": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@inquirer/input/-/input-4.1.3.tgz#fa0ea9a392b2ec4ddd763c504d0b0c8839a48fe2" + integrity sha512-zeo++6f7hxaEe7OjtMzdGZPHiawsfmCZxWB9X1NpmYgbeoyerIbWemvlBxxl+sQIlHC0WuSAG19ibMq3gbhaqQ== + dependencies: + "@inquirer/core" "^10.1.4" + "@inquirer/type" "^3.0.2" + +"@inquirer/number@^3.0.6": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@inquirer/number/-/number-3.0.6.tgz#19bba46725df194bdd907762cf432a37e053b300" + integrity sha512-xO07lftUHk1rs1gR0KbqB+LJPhkUNkyzV/KhH+937hdkMazmAYHLm1OIrNKpPelppeV1FgWrgFDjdUD8mM+XUg== + dependencies: + "@inquirer/core" "^10.1.4" + "@inquirer/type" "^3.0.2" + +"@inquirer/password@^4.0.6": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@inquirer/password/-/password-4.0.6.tgz#4bbee12fe7cd1d37435401098c296ddc4586861b" + integrity sha512-QLF0HmMpHZPPMp10WGXh6F+ZPvzWE7LX6rNoccdktv/Rov0B+0f+eyXkAcgqy5cH9V+WSpbLxu2lo3ysEVK91w== + dependencies: + "@inquirer/core" "^10.1.4" + "@inquirer/type" "^3.0.2" + ansi-escapes "^4.3.2" + +"@inquirer/prompts@^7.2.3": + version "7.2.3" + resolved "https://registry.yarnpkg.com/@inquirer/prompts/-/prompts-7.2.3.tgz#8a0d7cb5310d429bf815d25bbff108375fc6315b" + integrity sha512-hzfnm3uOoDySDXfDNOm9usOuYIaQvTgKp/13l1uJoe6UNY+Zpcn2RYt0jXz3yA+yemGHvDOxVzqWl3S5sQq53Q== + dependencies: + "@inquirer/checkbox" "^4.0.6" + "@inquirer/confirm" "^5.1.3" + "@inquirer/editor" "^4.2.3" + "@inquirer/expand" "^4.0.6" + "@inquirer/input" "^4.1.3" + "@inquirer/number" "^3.0.6" + "@inquirer/password" "^4.0.6" + "@inquirer/rawlist" "^4.0.6" + "@inquirer/search" "^3.0.6" + "@inquirer/select" "^4.0.6" + +"@inquirer/rawlist@^4.0.6": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@inquirer/rawlist/-/rawlist-4.0.6.tgz#b55d5828d850f07bc6792bbce3b2a963e33b3ef5" + integrity sha512-QoE4s1SsIPx27FO4L1b1mUjVcoHm1pWE/oCmm4z/Hl+V1Aw5IXl8FYYzGmfXaBT0l/sWr49XmNSiq7kg3Kd/Lg== + dependencies: + "@inquirer/core" "^10.1.4" + "@inquirer/type" "^3.0.2" + yoctocolors-cjs "^2.1.2" + +"@inquirer/search@^3.0.6": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@inquirer/search/-/search-3.0.6.tgz#5537e3f46b7d31ab65ca22b831cf546f88db1d5b" + integrity sha512-eFZ2hiAq0bZcFPuFFBmZEtXU1EarHLigE+ENCtpO+37NHCl4+Yokq1P/d09kUblObaikwfo97w+0FtG/EXl5Ng== + dependencies: + "@inquirer/core" "^10.1.4" + "@inquirer/figures" "^1.0.9" + "@inquirer/type" "^3.0.2" + yoctocolors-cjs "^2.1.2" + +"@inquirer/select@^4.0.6": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@inquirer/select/-/select-4.0.6.tgz#3062c02c82f7bbe238972672def6d8394732bb2b" + integrity sha512-yANzIiNZ8fhMm4NORm+a74+KFYHmf7BZphSOBovIzYPVLquseTGEkU5l2UTnBOf5k0VLmTgPighNDLE9QtbViQ== + dependencies: + "@inquirer/core" "^10.1.4" + "@inquirer/figures" "^1.0.9" + "@inquirer/type" "^3.0.2" + ansi-escapes "^4.3.2" + yoctocolors-cjs "^2.1.2" + +"@inquirer/type@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-3.0.2.tgz#baff9f8d70947181deb36772cd9a5b6876d3e60c" + integrity sha512-ZhQ4TvhwHZF+lGhQ2O/rsjo80XoZR5/5qhOY3t6FJuX5XBg5Be8YzYTvaUGJnc12AUGI2nr4QSUE4PhKSigx7g== + +"@ioredis/commands@^1.1.1": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11" + integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg== + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + 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" + +"@isaacs/fs-minipass@^4.0.0": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz#2d59ae3ab4b38fb4270bfa23d30f8e2e86c7fe32" + integrity sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w== + dependencies: + minipass "^7.0.4" + +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + 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" + +"@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@jest/console@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.7.0.tgz#cd4822dbdb84529265c5a2bdb529a3c9cc950ffc" + integrity sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg== + 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" + +"@jest/core@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.7.0.tgz#b6cccc239f30ff36609658c5a5e2291757ce448f" + integrity sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg== + 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" + +"@jest/environment@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.7.0.tgz#24d61f54ff1f786f3cd4073b4b94416383baf2a7" + integrity sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw== + dependencies: + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-mock "^29.7.0" + +"@jest/expect-utils@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz#023efe5d26a8a70f21677d0a1afc0f0a44e3a1c6" + integrity sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA== + dependencies: + jest-get-type "^29.6.3" + +"@jest/expect@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.7.0.tgz#76a3edb0cb753b70dfbfe23283510d3d45432bf2" + integrity sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ== + dependencies: + expect "^29.7.0" + jest-snapshot "^29.7.0" + +"@jest/fake-timers@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.7.0.tgz#fd91bf1fffb16d7d0d24a426ab1a47a49881a565" + integrity sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ== + 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" + +"@jest/globals@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.7.0.tgz#8d9290f9ec47ff772607fa864ca1d5a2efae1d4d" + integrity sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/types" "^29.6.3" + jest-mock "^29.7.0" + +"@jest/reporters@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.7.0.tgz#04b262ecb3b8faa83b0b3d321623972393e8f4c7" + integrity sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg== + 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" + +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== + dependencies: + "@sinclair/typebox" "^0.27.8" + +"@jest/source-map@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.6.3.tgz#d90ba772095cf37a34a5eb9413f1b562a08554c4" + integrity sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.18" + callsites "^3.0.0" + graceful-fs "^4.2.9" + +"@jest/test-result@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.7.0.tgz#8db9a80aa1a097bb2262572686734baed9b1657c" + integrity sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA== + dependencies: + "@jest/console" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + +"@jest/test-sequencer@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz#6cef977ce1d39834a3aea887a1726628a6f072ce" + integrity sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw== + dependencies: + "@jest/test-result" "^29.7.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + slash "^3.0.0" + +"@jest/transform@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.7.0.tgz#df2dd9c346c7d7768b8a06639994640c642e284c" + integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== + 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" + +"@jest/types@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" + integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== + 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" + +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.8" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz#4f0e06362e01362f823d348f1872b08f666d8142" + integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/source-map@^0.3.3": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@ljharb/through@^2.3.12": + version "2.3.13" + resolved "https://registry.yarnpkg.com/@ljharb/through/-/through-2.3.13.tgz#b7e4766e0b65aa82e529be945ab078de79874edc" + integrity sha512-/gKJun8NNiWGZJkGzI/Ragc53cOdcLNdzjLaIa+GEjguQs0ulsurx8WN0jijdK9yPqDvziX995sMRLyLt1uZMQ== + dependencies: + call-bind "^1.0.7" + +"@lukeed/csprng@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@lukeed/csprng/-/csprng-1.1.0.tgz#1e3e4bd05c1cc7a0b2ddbd8a03f39f6e4b5e6cfe" + integrity sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA== + +"@microsoft/tsdoc@^0.15.0": + version "0.15.1" + resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz#d4f6937353bc4568292654efb0a0e0532adbcba2" + integrity sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw== + +"@nestjs-modules/ioredis@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@nestjs-modules/ioredis/-/ioredis-2.0.2.tgz#b1799944c4932e26d24a930caad83cc7af34601d" + integrity sha512-8pzSvT8R3XP6p8ZzQmEN8OnY0yWrJ/elFhwQK+PID2zf1SLBkAZ18bDcx3SKQ2atledt0gd9kBeP5xT4MlyS7Q== + optionalDependencies: + "@nestjs/terminus" "10.2.0" + +"@nestjs/axios@^3.1.2": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@nestjs/axios/-/axios-3.1.3.tgz#cf73f317f89800ec2f6f04b577677617c5aef0d9" + integrity sha512-RZ/63c1tMxGLqyG3iOCVt7A72oy4x1eM6QEhd4KzCYpaVWW0igq0WSREeRoEZhIxRcZfDfIIkvsOMiM7yfVGZQ== + +"@nestjs/cli@^10.4.8": + version "10.4.9" + resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-10.4.9.tgz#ac3a23096a4725465360d8d60810f3e857f4a803" + integrity sha512-s8qYd97bggqeK7Op3iD49X2MpFtW4LVNLAwXFkfbRxKME6IYT7X0muNTJ2+QfI8hpbNx9isWkrLWIp+g5FOhiA== + 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" + +"@nestjs/common@^10.0.0": + version "10.4.15" + resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-10.4.15.tgz#27c291466d9100eb86fdbe6f7bbb4d1a6ad55f70" + integrity sha512-vaLg1ZgwhG29BuLDxPA9OAcIlgqzp9/N8iG0wGapyUNTf4IY4O6zAHgN6QalwLhFxq7nOI021vdRojR1oF3bqg== + dependencies: + uid "2.0.2" + iterare "1.2.1" + tslib "2.8.1" + +"@nestjs/config@^3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@nestjs/config/-/config-3.3.0.tgz#ddc520ba26a8453ee5e690e18fb7b35e9bac7974" + integrity sha512-pdGTp8m9d0ZCrjTpjkUbZx6gyf2IKf+7zlkrPNMsJzYZ4bFRRTpXrnj+556/5uiI6AfL5mMrJc2u7dB6bvM+VA== + dependencies: + dotenv "16.4.5" + dotenv-expand "10.0.0" + lodash "4.17.21" + +"@nestjs/core@^10.0.0": + version "10.4.15" + resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-10.4.15.tgz#1343a3395d5c54e9b792608cb75eef39053806d5" + integrity sha512-UBejmdiYwaH6fTsz2QFBlC1cJHM+3UDeLZN+CiP9I1fRv2KlBZsmozGLbV5eS1JAVWJB4T5N5yQ0gjN8ZvcS2w== + dependencies: + uid "2.0.2" + "@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" + +"@nestjs/mapped-types@*": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz#b9b536b7c3571567aa1d0223db8baa1a51505a19" + integrity sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw== + +"@nestjs/mapped-types@2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@nestjs/mapped-types/-/mapped-types-2.0.6.tgz#d2d8523709fd5d872a9b9e0c38162746e2a7f44e" + integrity sha512-84ze+CPfp1OWdpRi1/lOu59hOhTz38eVzJvRKrg9ykRFwDz+XleKfMsG0gUqNZYFa6v53XYzeD+xItt8uDW7NQ== + +"@nestjs/platform-express@^10.0.0": + version "10.4.15" + resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-10.4.15.tgz#117fe7470c2b93e4ac07687b7fa604a17ca240c5" + integrity sha512-63ZZPkXHjoDyO7ahGOVcybZCRa7/Scp6mObQKjcX/fTEq1YJeU75ELvMsuQgc8U2opMGOBD7GVuc4DV0oeDHoA== + dependencies: + body-parser "1.20.3" + cors "2.8.5" + express "4.21.2" + multer "1.4.4-lts.1" + tslib "2.8.1" + +"@nestjs/schematics@^10.0.0", "@nestjs/schematics@^10.0.1": + version "10.2.3" + resolved "https://registry.yarnpkg.com/@nestjs/schematics/-/schematics-10.2.3.tgz#6053f43c5065b9e825cd08c4db1bf6bcbc9a6a62" + integrity sha512-4e8gxaCk7DhBxVUly2PjYL4xC2ifDFexCqq1/u4TtivLGXotVk0wHdYuPYe1tHTHuR1lsOkRbfOCpkdTnigLVg== + 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" + +"@nestjs/swagger@^8.1.0": + version "8.1.1" + resolved "https://registry.yarnpkg.com/@nestjs/swagger/-/swagger-8.1.1.tgz#18d61a995b6b17759bca361474904d69c4406093" + integrity sha512-5Mda7H1DKnhKtlsb0C7PYshcvILv8UFyUotHzxmWh0G65Z21R3LZH/J8wmpnlzL4bmXIfr42YwbEwRxgzpJ5sQ== + 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" + +"@nestjs/terminus@10.2.0": + version "10.2.0" + resolved "https://registry.yarnpkg.com/@nestjs/terminus/-/terminus-10.2.0.tgz#3776ac21d2b1a1b8ce0a60e7f51dd6c498ca1de6" + integrity sha512-zPs98xvJ4ogEimRQOz8eU90mb7z+W/kd/mL4peOgrJ/VqER+ibN2Cboj65uJZW3XuNhpOqaeYOJte86InJd44A== + dependencies: + boxen "5.1.2" + check-disk-space "3.4.0" + +"@nestjs/testing@^10.4.15": + version "10.4.15" + resolved "https://registry.yarnpkg.com/@nestjs/testing/-/testing-10.4.15.tgz#4c9fe17bf026c0142040cbe3db464c526f89d36a" + integrity sha512-eGlWESkACMKti+iZk1hs6FUY/UqObmMaa8HAN9JLnaYkoLf1Jeh+EuHlGnfqo/Rq77oznNLIyaA3PFjrFDlNUg== + dependencies: + tslib "2.8.1" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@nuxtjs/opencollective@0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz#620ce1044f7ac77185e825e1936115bb38e2681c" + integrity sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA== + dependencies: + chalk "^4.1.0" + consola "^2.15.0" + node-fetch "^2.6.1" + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +"@pkgr/core@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" + integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== + +"@scarf/scarf@=1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@scarf/scarf/-/scarf-1.4.0.tgz#3bbb984085dbd6d982494538b523be1ce6562972" + integrity sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ== + +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + +"@sinonjs/commons@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" + integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^10.0.2": + version "10.3.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz#55fdff1ecab9f354019129daf4df0dd4d923ea66" + integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== + dependencies: + "@sinonjs/commons" "^3.0.0" + +"@sitespeed.io/throttle@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@sitespeed.io/throttle/-/throttle-3.1.1.tgz#7e0b260e13160496e1c20df00e212751bda3258d" + integrity sha512-6KOsYUQgAur5636IiGYblcRGOiiBAEVao+imE+VkEeBBIGcYUwu4//oXkwdG2WjjE+2gS4AsMzfyHTlv6zXUfg== + dependencies: + minimist "1.2.6" + +"@tsconfig/node10@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" + integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + +"@types/babel__core@^7.1.14", "@types/babel__core@^7.20.5": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" + integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.8" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.8.tgz#f836c61f48b1346e7d2b0d93c6dacc5b9535d3ab" + integrity sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" + integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": + version "7.20.6" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.6.tgz#8dc9f0ae0f202c08d8d4dab648912c8d6038e3f7" + integrity sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg== + dependencies: + "@babel/types" "^7.20.7" + +"@types/body-parser@*": + version "1.19.5" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/cookiejar@^2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.5.tgz#14a3e83fa641beb169a2dd8422d91c3c345a9a78" + integrity sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q== + +"@types/debug@^4.1.12": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" + integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== + dependencies: + "@types/ms" "*" + +"@types/eslint-scope@^3.7.7": + version "3.7.7" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" + integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "9.6.1" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-9.6.1.tgz#d5795ad732ce81715f27f75da913004a56751584" + integrity sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@^1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== + +"@types/express-serve-static-core@^4.17.33": + version "4.19.6" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz#e01324c2a024ff367d92c66f48553ced0ab50267" + integrity sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@^4.17.17": + version "4.17.21" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/graceful-fs@^4.1.3": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4" + integrity sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ== + dependencies: + "@types/node" "*" + +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + +"@types/ini@^4.1.1": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@types/ini/-/ini-4.1.1.tgz#6984664a8cc74c3348f4049d0bf2b1ab2d061ca3" + integrity sha512-MIyNUZipBTbyUNnhvuXJTY7B6qNI78meck9Jbv3wk0OgNwRyOOVEKDutAkOs1snB/tx0FafyR6/SN4Ps0hZPeg== + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" + integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== + +"@types/istanbul-lib-report@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz#53047614ae72e19fc0401d872de3ae2b4ce350bf" + integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54" + integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/jest@^29.5.14": + version "29.5.14" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.14.tgz#2b910912fa1d6856cadcd0c1f95af7df1d6049e5" + integrity sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ== + dependencies: + expect "^29.0.0" + pretty-format "^29.0.0" + +"@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/methods@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@types/methods/-/methods-1.1.4.tgz#d3b7ac30ac47c91054ea951ce9eed07b1051e547" + integrity sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ== + +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/ms@*": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" + integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== + +"@types/node-forge@^1.3.0": + version "1.3.11" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" + integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== + dependencies: + "@types/node" "*" + +"@types/node@*", "@types/node@^22.10.1": + version "22.10.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.10.tgz#85fe89f8bf459dc57dfef1689bd5b52ad1af07e6" + integrity sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww== + dependencies: + undici-types "~6.20.0" + +"@types/protoo-server@^4.0.6": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@types/protoo-server/-/protoo-server-4.0.6.tgz#3106d43ff1d3ef9cdd3726de191ee552029b7c4e" + integrity sha512-bqRjUxS2L+jzMn3dC0bT0IKVz7McjrsYZb0FIFnca5EpNusr4ar20fTY+hfXuE03Xet+esBT62bX69LWm9Q4zA== + dependencies: + "@types/node" "*" + "@types/websocket" "*" + +"@types/qs@*": + version "6.9.18" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.18.tgz#877292caa91f7c1b213032b34626505b746624c2" + integrity sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/semver@^7.5.0": + version "7.5.8" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" + integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== + +"@types/send@*": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-static@*": + version "1.15.7" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" + integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "*" + +"@types/stack-utils@^2.0.0": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" + integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== + +"@types/superagent@^8.1.0": + version "8.1.9" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-8.1.9.tgz#28bfe4658e469838ed0bf66d898354bcab21f49f" + integrity sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ== + dependencies: + "@types/cookiejar" "^2.1.5" + "@types/methods" "^1.1.4" + "@types/node" "*" + form-data "^4.0.0" + +"@types/supertest@^6.0.0": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-6.0.2.tgz#2af1c466456aaf82c7c6106c6b5cbd73a5e86588" + integrity sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg== + dependencies: + "@types/methods" "^1.1.4" + "@types/superagent" "^8.1.0" + +"@types/validator@^13.11.8": + version "13.12.2" + resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.12.2.tgz#760329e756e18a4aab82fc502b51ebdfebbe49f5" + integrity sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA== + +"@types/websocket@*": + version "1.0.10" + resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-1.0.10.tgz#804b1a02780da522f5742bc184a6d16a2eb78c7c" + integrity sha512-svjGZvPB7EzuYS94cI7a+qhwgGU1y89wUgjT6E2wVUfmAGIvRfT7obBvRtnhXCSsoMdlG4gBFGE7MfkIXZLoww== + dependencies: + "@types/node" "*" + +"@types/yargs-parser@*": + version "21.0.3" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" + integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== + +"@types/yargs@^17.0.8": + version "17.0.33" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.33.tgz#8c32303da83eec050a84b3c7ae7b9f922d13e32d" + integrity sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA== + dependencies: + "@types/yargs-parser" "*" + +"@typescript-eslint/eslint-plugin@^6.0.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz#30830c1ca81fd5f3c2714e524c4303e0194f9cd3" + integrity sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA== + dependencies: + "@eslint-community/regexpp" "^4.5.1" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/type-utils" "6.21.0" + "@typescript-eslint/utils" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + debug "^4.3.4" + graphemer "^1.4.0" + ignore "^5.2.4" + natural-compare "^1.4.0" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/parser@^6.0.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" + integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== + dependencies: + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/typescript-estree" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" + integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== + dependencies: + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + +"@typescript-eslint/type-utils@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz#6473281cfed4dacabe8004e8521cee0bd9d4c01e" + integrity sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag== + dependencies: + "@typescript-eslint/typescript-estree" "6.21.0" + "@typescript-eslint/utils" "6.21.0" + debug "^4.3.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/types@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" + integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== + +"@typescript-eslint/typescript-estree@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" + integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== + dependencies: + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + minimatch "9.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/utils@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.21.0.tgz#4714e7a6b39e773c1c8e97ec587f520840cd8134" + integrity sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/typescript-estree" "6.21.0" + semver "^7.5.4" + +"@typescript-eslint/visitor-keys@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" + integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== + dependencies: + "@typescript-eslint/types" "6.21.0" + eslint-visitor-keys "^3.4.1" + +"@ungap/structured-clone@^1.2.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" + integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== + +"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6" + integrity sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ== + dependencies: + "@webassemblyjs/helper-numbers" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + +"@webassemblyjs/floating-point-hex-parser@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz#fcca1eeddb1cc4e7b6eed4fc7956d6813b21b9fb" + integrity sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA== + +"@webassemblyjs/helper-api-error@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz#e0a16152248bc38daee76dd7e21f15c5ef3ab1e7" + integrity sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ== + +"@webassemblyjs/helper-buffer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz#822a9bc603166531f7d5df84e67b5bf99b72b96b" + integrity sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA== + +"@webassemblyjs/helper-numbers@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz#dbd932548e7119f4b8a7877fd5a8d20e63490b2d" + integrity sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.13.2" + "@webassemblyjs/helper-api-error" "1.13.2" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz#e556108758f448aae84c850e593ce18a0eb31e0b" + integrity sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA== + +"@webassemblyjs/helper-wasm-section@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz#9629dda9c4430eab54b591053d6dc6f3ba050348" + integrity sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw== + 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" + +"@webassemblyjs/ieee754@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz#1c5eaace1d606ada2c7fd7045ea9356c59ee0dba" + integrity sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.13.2.tgz#57c5c3deb0105d02ce25fa3fd74f4ebc9fd0bbb0" + integrity sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.13.2.tgz#917a20e93f71ad5602966c2d685ae0c6c21f60f1" + integrity sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ== + +"@webassemblyjs/wasm-edit@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz#ac6689f502219b59198ddec42dcd496b1004d597" + integrity sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ== + 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" + +"@webassemblyjs/wasm-gen@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz#991e7f0c090cb0bb62bbac882076e3d219da9570" + integrity sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg== + 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" + +"@webassemblyjs/wasm-opt@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz#e6f71ed7ccae46781c206017d3c14c50efa8106b" + integrity sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" + +"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz#b3e13f1893605ca78b52c68e54cf6a865f90b9fb" + integrity sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ== + 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" + +"@webassemblyjs/wast-printer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz#3bb3e9638a8ae5fdaf9610e7a06b4d9f9aa6fe07" + integrity sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@xtuc/long" "4.2.2" + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn-walk@^8.1.1: + version "8.3.4" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" + integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== + dependencies: + acorn "^8.11.0" + +acorn@^8.11.0, acorn@^8.14.0, acorn@^8.4.1, acorn@^8.8.2, acorn@^8.9.0: + version "8.14.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" + integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== + +ajv-formats@2.1.1, ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv-keywords@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@8.12.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ajv@^6.12.4, ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + 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" + +ajv@^8.0.0, ajv@^8.9.0: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + +ansi-align@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" + integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== + dependencies: + string-width "^4.1.0" + +ansi-colors@4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== + +ansi-escapes@^4.2.1, ansi-escapes@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" + integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== + +ansi-styles@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +anymatch@^3.0.3, anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw== + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array-timsort@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-timsort/-/array-timsort-1.0.3.tgz#3c9e4199e54fb2b9c3fe5976396a21614ef0d926" + integrity sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ== + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +asap@^2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + +async@^3.2.3: + version "3.2.6" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +axios@^1.7.8: + version "1.7.9" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a" + integrity sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +babel-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" + integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== + 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" + +babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== + 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" + +babel-plugin-jest-hoist@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz#aadbe943464182a8922c3c927c3067ff40d24626" + integrity sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.1.14" + "@types/babel__traverse" "^7.0.6" + +babel-preset-current-node-syntax@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz#9a929eafece419612ef4ae4f60b1862ebad8ef30" + integrity sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw== + 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" + +babel-preset-jest@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz#fa05fa510e7d493896d7b0dd2033601c840f171c" + integrity sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA== + dependencies: + babel-plugin-jest-hoist "^29.6.3" + babel-preset-current-node-syntax "^1.0.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +bl@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + +body-parser@1.20.3: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.13.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +boxen@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50" + integrity sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ== + dependencies: + ansi-align "^3.0.0" + camelcase "^6.2.0" + chalk "^4.1.0" + cli-boxes "^2.2.1" + string-width "^4.2.2" + type-fest "^0.20.2" + widest-line "^3.1.0" + wrap-ansi "^7.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browserslist@^4.24.0: + version "4.24.4" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b" + integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A== + dependencies: + caniuse-lite "^1.0.30001688" + electron-to-chromium "^1.5.73" + node-releases "^2.0.19" + update-browserslist-db "^1.1.1" + +bs-logger@^0.2.6: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +bufferutil@^4.0.1: + version "4.0.9" + resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.9.tgz#6e81739ad48a95cad45a279588e13e95e24a800a" + integrity sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw== + dependencies: + node-gyp-build "^4.3.0" + +busboy@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz#32e5892e6361b29b0b545ba6f7763378daca2840" + integrity sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bind@^1.0.7: + version "1.0.8" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + 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" + +call-bound@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.3.tgz#41cfd032b593e39176a71533ab4f384aa04fd681" + integrity sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA== + dependencies: + call-bind-apply-helpers "^1.0.1" + get-intrinsic "^1.2.6" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001688: + version "1.0.30001695" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz#39dfedd8f94851132795fdf9b79d29659ad9c4d4" + integrity sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw== + +chalk@4.1.2, chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^5.3.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.4.1.tgz#1b48bf0963ec158dce2aacf69c093ae2dd2092d8" + integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w== + +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + +check-disk-space@3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/check-disk-space/-/check-disk-space-3.4.0.tgz#eb8e69eee7a378fd12e35281b8123a8b4c4a8ff7" + integrity sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw== + +chokidar@3.6.0, chokidar@^3.5.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + 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" + optionalDependencies: + fsevents "~2.3.2" + +chownr@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-3.0.0.tgz#9855e64ecd240a9cc4267ce8a4aa5d24a1da15e4" + integrity sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g== + +chrome-trace-event@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" + integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== + +ci-info@^3.2.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== + +cjs-module-lexer@^1.0.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz#707413784dbb3a72aa11c2f2b042a0bef4004170" + integrity sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA== + +class-transformer@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.5.1.tgz#24147d5dffd2a6cea930a3250a677addf96ab336" + integrity sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw== + +class-validator@^0.14.1: + version "0.14.1" + resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.14.1.tgz#ff2411ed8134e9d76acfeb14872884448be98110" + integrity sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ== + dependencies: + "@types/validator" "^13.11.8" + libphonenumber-js "^1.10.53" + validator "^13.9.0" + +cli-boxes@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" + integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-spinners@^2.5.0: + version "2.9.2" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" + integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== + +cli-table3@0.6.5: + version "0.6.5" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.5.tgz#013b91351762739c16a9567c21a04632e449bf2f" + integrity sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ== + dependencies: + string-width "^4.2.0" + optionalDependencies: + "@colors/colors" "1.5.0" + +cli-width@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" + integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== + +cli-width@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-4.1.0.tgz#42daac41d3c254ef38ad8ac037672130173691c5" + integrity sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ== + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== + +cluster-key-slot@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" + integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== + +collect-v8-coverage@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz#c0b29bcd33bcd0779a1344c2136051e6afd3d9e9" + integrity sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q== + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +comment-json@4.2.5: + version "4.2.5" + resolved "https://registry.yarnpkg.com/comment-json/-/comment-json-4.2.5.tgz#482e085f759c2704b60bc6f97f55b8c01bc41e70" + integrity sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw== + 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" + +component-emitter@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.1.tgz#ef1d5796f7d93f135ee6fb684340b26403c97d17" + integrity sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +concat-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +consola@^2.15.0: + version "2.15.3" + resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" + integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" + integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== + +cookiejar@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" + integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== + +core-util-is@^1.0.3, core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cors@2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cosmiconfig@^8.2.0: + version "8.3.6" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" + integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== + dependencies: + import-fresh "^3.3.0" + js-yaml "^4.1.0" + parse-json "^5.2.0" + path-type "^4.0.0" + +create-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320" + integrity sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q== + 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" + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +d@1, d@^1.0.1, d@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.2.tgz#2aefd554b81981e7dccf72d6842ae725cb17e5de" + integrity sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw== + dependencies: + es5-ext "^0.10.64" + type "^2.7.2" + +data-uri-to-buffer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" + integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== + +debug@2.6.9, debug@^2.2.0: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.7: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + +dedent@^1.0.0: + version "1.5.3" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.3.tgz#99aee19eb9bae55a67327717b6e848d0bf777e5a" + integrity sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ== + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + +defaults@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" + integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== + dependencies: + clone "^1.0.2" + +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +denque@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" + integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-newline@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + +dezalgo@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" + integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== + dependencies: + asap "^2.0.0" + wrappy "1" + +diff-sequences@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" + integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dotenv-expand@10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-10.0.0.tgz#12605d00fb0af6d0a592e6558585784032e4ef37" + integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A== + +dotenv@16.4.5: + version "16.4.5" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" + integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== + +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +ejs@^3.1.10: + version "3.1.10" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" + integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== + dependencies: + jake "^10.8.5" + +electron-to-chromium@^1.5.73: + version "1.5.87" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.87.tgz#3a89bec85e43a8b32445ec938228e4ec982e0f79" + integrity sha512-mPFwmEWmRivw2F8x3w3l2m6htAUN97Gy0kwpO++2m9iT1Gt8RCFVUfv9U/sIbHJ6rY4P6/ooqFL/eL7ock+pPg== + +emittery@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" + integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + +enhanced-resolve@^5.0.0, enhanced-resolve@^5.17.1, enhanced-resolve@^5.7.0: + version "5.18.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz#91eb1db193896b9801251eeff1c6980278b1e404" + integrity sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-define-property@^1.0.0, es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-module-lexer@^1.2.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.6.0.tgz#da49f587fd9e68ee2404fe4e256c0c7d3a81be21" + integrity sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ== + +es-object-atoms@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + +es5-ext@^0.10.35, es5-ext@^0.10.62, es5-ext@^0.10.63, es5-ext@^0.10.64, es5-ext@~0.10.14: + version "0.10.64" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.64.tgz#12e4ffb48f1ba2ea777f1fcdd1918ef73ea21714" + integrity sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg== + dependencies: + es6-iterator "^2.0.3" + es6-symbol "^3.1.3" + esniff "^2.0.1" + next-tick "^1.1.0" + +es6-iterator@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g== + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-symbol@^3.1.1, es6-symbol@^3.1.3: + version "3.1.4" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.4.tgz#f4e7d28013770b4208ecbf3e0bf14d3bcb557b8c" + integrity sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg== + dependencies: + d "^1.0.2" + ext "^1.7.0" + +escalade@^3.1.1, escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-config-prettier@^9.0.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz#31af3d94578645966c082fcb71a5846d3c94867f" + integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw== + +eslint-plugin-prettier@^5.0.0: + version "5.2.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz#c4af01691a6fa9905207f0fbba0d7bea0902cce5" + integrity sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw== + dependencies: + prettier-linter-helpers "^1.0.0" + synckit "^0.9.1" + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint@^8.42.0: + version "8.57.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" + integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.57.1" + "@humanwhocodes/config-array" "^0.13.0" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + "@ungap/structured-clone" "^1.2.0" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" + esquery "^1.4.2" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + strip-ansi "^6.0.1" + text-table "^0.2.0" + +esniff@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/esniff/-/esniff-2.0.1.tgz#a4d4b43a5c71c7ec51c51098c1d8a29081f9b308" + integrity sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg== + dependencies: + d "^1.0.1" + es5-ext "^0.10.62" + event-emitter "^0.3.5" + type "^2.7.2" + +espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== + dependencies: + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" + +esprima@^4.0.0, esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.4.2: + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +event-emitter@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + integrity sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA== + dependencies: + d "1" + es5-ext "~0.10.14" + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + 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" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== + +expect@^29.0.0, expect@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" + integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== + 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" + +express@4.21.2: + version "4.21.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" + integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.3" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.7.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.3.1" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.3" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.12" + proxy-addr "~2.0.7" + qs "6.13.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.19.0" + serve-static "1.16.2" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +ext@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" + integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw== + dependencies: + type "^2.7.2" + +external-editor@^3.0.3, external-editor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-diff@^1.1.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" + integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== + +fast-glob@^3.2.9: + version "3.3.3" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.8" + +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fast-safe-stringify@2.1.1, fast-safe-stringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + +fast-uri@^3.0.1: + version "3.0.6" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" + integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== + +fastq@^1.6.0: + version "1.18.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.18.0.tgz#d631d7e25faffea81887fe5ea8c9010e1b36fee0" + integrity sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw== + dependencies: + reusify "^1.0.4" + +fb-watchman@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" + integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== + dependencies: + bser "2.1.1" + +fetch-blob@^3.1.2, fetch-blob@^3.1.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" + integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + +figures@^3.0.0, figures@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +filelist@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" + integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== + dependencies: + minimatch "^5.0.1" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== + dependencies: + debug "2.6.9" + encodeurl "~2.0.0" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.3" + rimraf "^3.0.2" + +flatbuffers@^24.3.25: + version "24.12.23" + resolved "https://registry.yarnpkg.com/flatbuffers/-/flatbuffers-24.12.23.tgz#6eea59d2bcda0c5d59bcacefd6216348b3086883" + integrity sha512-dLVCAISd5mhls514keQzmEG6QHmUUsNuWsb4tFafIUwvvgDjXhtfAYSKOzt5SWOy+qByV5pbsDZ+Vb7HUOBEdA== + +flatted@^3.2.9: + version "3.3.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.2.tgz#adba1448a9841bec72b42c532ea23dbbedef1a27" + integrity sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA== + +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + +foreground-child@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" + integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + +fork-ts-checker-webpack-plugin@9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.0.2.tgz#c12c590957837eb02b02916902dcf3e675fd2b1e" + integrity sha512-Uochze2R8peoN1XqlSi/rGUkDQpRogtLFocP9+PGu68zk1BDAKXfdeCdyVZpgTk8V8WFVQXdEz426VKjXLO1Gg== + 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" + +form-data@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48" + integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +formdata-polyfill@^4.0.10: + version "4.0.10" + resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" + integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== + dependencies: + fetch-blob "^3.1.2" + +formidable@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.1.2.tgz#fa973a2bec150e4ce7cac15589d7a25fc30ebd89" + integrity sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g== + dependencies: + dezalgo "^1.0.4" + hexoid "^1.0.0" + once "^1.4.0" + qs "^6.11.0" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-extra@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-monkey@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.6.tgz#8ead082953e88d992cf3ff844faa907b26756da2" + integrity sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@^2.3.2, fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6: + version "1.2.7" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.7.tgz#dcfcb33d3272e15f445d15124bc0a216189b9044" + integrity sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + function-bind "^1.1.2" + get-proto "^1.0.0" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + +get-proto@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@10.4.5, glob@^10.3.7: + version "10.4.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== + 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" + +glob@^7.1.3, glob@^7.1.4: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + 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" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^13.19.0: + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== + dependencies: + type-fest "^0.20.2" + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +gopd@^1.0.1, gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + +h264-profile-level-id@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/h264-profile-level-id/-/h264-profile-level-id-2.0.0.tgz#b7ea45badbac8f5dbb9583d34b06db09764f2535" + integrity sha512-X4CLryVbVA0CtjTExS4G5U1gb2Z4wa32AF8ukVmFuLdw2JRq2aHisor7SY5SYTUUrUSqq0KdPIO18sql6IWIQw== + dependencies: + "@types/debug" "^4.1.12" + debug "^4.3.4" + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-own-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-own-prop/-/has-own-prop-2.0.0.tgz#f0f95d58f65804f5d218db32563bb85b8e0417af" + integrity sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ== + +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +hexoid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" + integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== + +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +iconv-lite@0.4.24, iconv-lite@^0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore@^5.2.0, ignore@^5.2.4: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + +import-fresh@^3.2.1, import-fresh@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-local@^3.0.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" + integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ini/-/ini-5.0.0.tgz#a7a4615339843d9a8ccc2d85c9d81cf93ffbc638" + integrity sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw== + +inquirer@8.2.6: + version "8.2.6" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.6.tgz#733b74888195d8d400a67ac332011b5fae5ea562" + integrity sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg== + 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" + +inquirer@9.2.15: + version "9.2.15" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-9.2.15.tgz#2135a36190a6e5c92f5d205e0af1fea36b9d3492" + integrity sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg== + 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" + +inquirer@^12.1.0: + version "12.3.2" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-12.3.2.tgz#afa6e8573f9e9ca408335b4936ae32cbea708c22" + integrity sha512-YjQCIcDd3yyDuQrbII0FBtm/ZqNoWtvaC71yeCnd5Vbg4EgzsAGaemzfpzmqfvIZEp2roSwuZZKdM0C65hA43g== + dependencies: + "@inquirer/core" "^10.1.4" + "@inquirer/prompts" "^7.2.3" + "@inquirer/type" "^3.0.2" + ansi-escapes "^4.3.2" + mute-stream "^2.0.0" + run-async "^3.0.0" + rxjs "^7.8.1" + +ioredis@^5.4.1: + version "5.4.2" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.4.2.tgz#ebb6f1a10b825b2c0fb114763d7e82114a0bee6c" + integrity sha512-0SZXGNGZ+WzISQ67QDyZ2x0+wVxjjUndtD8oSeik/4ajifeiRufed8fCb8QW8VMyi4MXcS+UO1k/0NGhvq1PAg== + dependencies: + "@ioredis/commands" "^1.1.1" + cluster-key-slot "^1.1.0" + debug "^4.3.4" + denque "^2.1.0" + lodash.defaults "^4.2.0" + lodash.isarguments "^3.1.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.1.0" + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.16.0: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== + dependencies: + hasown "^2.0.2" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-interactive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" + integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-typedarray@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" + integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== + +istanbul-lib-instrument@^5.0.4: + version "5.2.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" + integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== + 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" + +istanbul-lib-instrument@^6.0.0: + version "6.0.3" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz#fa15401df6c15874bcb2105f773325d78c666765" + integrity sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q== + 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" + +istanbul-lib-report@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d" + integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^4.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + source-map "^0.6.1" + +istanbul-reports@^3.1.3: + version "3.1.7" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz#daed12b9e1dca518e15c056e1e537e741280fa0b" + integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + +iterare@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/iterare/-/iterare-1.2.1.tgz#139c400ff7363690e33abffa33cbba8920f00042" + integrity sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q== + +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +jake@^10.8.5: + version "10.9.2" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.2.tgz#6ae487e6a69afec3a5e167628996b59f35ae2b7f" + integrity sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA== + dependencies: + async "^3.2.3" + chalk "^4.0.2" + filelist "^1.0.4" + minimatch "^3.1.2" + +jest-changed-files@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a" + integrity sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w== + dependencies: + execa "^5.0.0" + jest-util "^29.7.0" + p-limit "^3.1.0" + +jest-circus@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.7.0.tgz#b6817a45fcc835d8b16d5962d0c026473ee3668a" + integrity sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw== + 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" + +jest-cli@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.7.0.tgz#5592c940798e0cae677eec169264f2d839a37995" + integrity sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg== + 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" + +jest-config@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.7.0.tgz#bcbda8806dbcc01b1e316a46bb74085a84b0245f" + integrity sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ== + 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" + +jest-diff@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" + integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.6.3" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-docblock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.7.0.tgz#8fddb6adc3cdc955c93e2a87f61cfd350d5d119a" + integrity sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g== + dependencies: + detect-newline "^3.0.0" + +jest-each@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.7.0.tgz#162a9b3f2328bdd991beaabffbb74745e56577d1" + integrity sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ== + 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" + +jest-environment-node@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.7.0.tgz#0b93e111dda8ec120bc8300e6d1fb9576e164376" + integrity sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw== + 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" + +jest-get-type@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" + integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== + +jest-haste-map@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.7.0.tgz#3c2396524482f5a0506376e6c858c3bbcc17b104" + integrity sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA== + 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" + optionalDependencies: + fsevents "^2.3.2" + +jest-leak-detector@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz#5b7ec0dadfdfec0ca383dc9aa016d36b5ea4c728" + integrity sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw== + dependencies: + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-matcher-utils@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz#ae8fec79ff249fd592ce80e3ee474e83a6c44f12" + integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== + dependencies: + chalk "^4.0.0" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-message-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3" + integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== + 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" + +jest-mock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.7.0.tgz#4e836cf60e99c6fcfabe9f99d017f3fdd50a6347" + integrity sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-util "^29.7.0" + +jest-pnp-resolver@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" + integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== + +jest-regex-util@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz#4a556d9c776af68e1c5f48194f4d0327d24e8a52" + integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== + +jest-resolve-dependencies@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz#1b04f2c095f37fc776ff40803dc92921b1e88428" + integrity sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA== + dependencies: + jest-regex-util "^29.6.3" + jest-snapshot "^29.7.0" + +jest-resolve@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.7.0.tgz#64d6a8992dd26f635ab0c01e5eef4399c6bcbc30" + integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== + 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" + +jest-runner@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.7.0.tgz#809af072d408a53dcfd2e849a4c976d3132f718e" + integrity sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ== + 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" + +jest-runtime@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.7.0.tgz#efecb3141cf7d3767a3a0cc8f7c9990587d3d817" + integrity sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ== + 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" + +jest-snapshot@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.7.0.tgz#c2c574c3f51865da1bb329036778a69bf88a6be5" + integrity sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw== + 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" + +jest-util@^29.0.0, jest-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" + integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== + 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" + +jest-validate@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.7.0.tgz#7bf705511c64da591d46b15fce41400d52147d9c" + integrity sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw== + 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" + +jest-watcher@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.7.0.tgz#7810d30d619c3a62093223ce6bb359ca1b28a2f2" + integrity sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g== + 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" + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest-worker@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a" + integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== + dependencies: + "@types/node" "*" + jest-util "^29.7.0" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.7.0.tgz#994676fc24177f088f1c5e3737f5697204ff2613" + integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== + dependencies: + "@jest/core" "^29.7.0" + "@jest/types" "^29.6.3" + import-local "^3.0.2" + jest-cli "^29.7.0" + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@4.1.0, js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +json5@^2.2.2, json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +jsonc-parser@3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.1.tgz#031904571ccf929d7670ee8c547545081cb37f1a" + integrity sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA== + +jsonc-parser@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.3.1.tgz#f2a524b4f7fd11e3d791e559977ad60b98b798b4" + integrity sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +libphonenumber-js@^1.10.53: + version "1.11.18" + resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.11.18.tgz#0ce5188a76487fae01afdf318255783cde36501e" + integrity sha512-okMm/MCoFrm1vByeVFLBdkFIXLSHy/AIK2AEGgY3eoicfWZeOZqv3GfhtQgICkzs/tqorAMm3a4GBg5qNCrqzg== + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== + +lodash.isarguments@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg== + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash@4.17.21, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +magic-string@0.30.8: + version "0.30.8" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.8.tgz#14e8624246d2bedba70d5462aa99ac9681844613" + integrity sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + +make-dir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" + integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== + dependencies: + semver "^7.5.3" + +make-error@^1.1.1, make-error@^1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== + dependencies: + tmpl "1.0.5" + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +mediasoup@^3.15.2: + version "3.15.2" + resolved "https://registry.yarnpkg.com/mediasoup/-/mediasoup-3.15.2.tgz#cacdff390ffdfcd481e2162990f1e882b652ab21" + integrity sha512-H9jEo7WcW97FQpylNo+xTW/DxTtN/pqYdjT644yJ7DiLx5ww1u/3V0InSTSpMfYkWM0u6XzqJ1+pU+oO/Oxh8w== + dependencies: + "@types/ini" "^4.1.1" + debug "^4.3.7" + flatbuffers "^24.3.25" + h264-profile-level-id "^2.0.0" + ini "^5.0.0" + node-fetch "^3.3.2" + supports-color "^9.4.0" + tar "^7.4.3" + +memfs@^3.4.1: + version "3.6.0" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" + integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ== + dependencies: + fs-monkey "^1.0.4" + +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +methods@^1.1.2, methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^4.0.0, micromatch@^4.0.4, micromatch@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mime@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimatch@9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +minimist@1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + +minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.4, minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + +minizlib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-3.0.1.tgz#46d5329d1eb3c83924eff1d3b858ca0a31581012" + integrity sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg== + dependencies: + minipass "^7.0.4" + rimraf "^5.0.5" + +mkdirp@^0.5.4: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mkdirp@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" + integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multer@1.4.4-lts.1: + version "1.4.4-lts.1" + resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.4-lts.1.tgz#24100f701a4611211cfae94ae16ea39bb314e04d" + integrity sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg== + dependencies: + append-field "^1.0.0" + busboy "^1.0.0" + concat-stream "^1.5.2" + mkdirp "^0.5.4" + object-assign "^4.1.1" + type-is "^1.6.4" + xtend "^4.0.0" + +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + +mute-stream@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-1.0.0.tgz#e31bd9fe62f0aed23520aa4324ea6671531e013e" + integrity sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA== + +mute-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-2.0.0.tgz#a5446fc0c512b71c83c44d908d5c7b7b4c493b2b" + integrity sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== + +node-abort-controller@^3.0.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" + integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ== + +node-domexception@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + +node-emoji@1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c" + integrity sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A== + dependencies: + lodash "^4.17.21" + +node-fetch@^2.6.1: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +node-fetch@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b" + integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== + dependencies: + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.4" + formdata-polyfill "^4.0.10" + +node-forge@^1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + +node-gyp-build@^4.3.0: + version "4.8.4" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8" + integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ== + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== + +node-releases@^2.0.19: + version "2.0.19" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" + integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +object-assign@^4, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.3: + version "1.13.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.3.tgz#f14c183de51130243d6d18ae149375ff50ea488a" + integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +once@^1.3.0, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.0, onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== + 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" + +ora@5.4.1, ora@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" + integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== + 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" + +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2, p-limit@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + 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" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +path-to-regexp@0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" + integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== + +path-to-regexp@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b" + integrity sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picocolors@^1.0.0, picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.1.tgz#68c26c8837399e5819edce48590412ea07f17a07" + integrity sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pirates@^4.0.4: + version "4.0.6" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" + integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +pluralize@8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" + integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== + dependencies: + fast-diff "^1.1.2" + +prettier@^3.0.0: + version "3.4.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.4.2.tgz#a5ce1fb522a588bf2b78ca44c6e6fe5aa5a2b13f" + integrity sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ== + +pretty-format@^29.0.0, pretty-format@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" + integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== + dependencies: + "@jest/schemas" "^29.6.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +prompts@^2.0.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +protoo-client@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/protoo-client/-/protoo-client-4.0.6.tgz#02a89f997ee5a4f385dab7be938dda1a2c5158e4" + integrity sha512-ZqImkKHpeJhSlgvyI6QAfZNc/aXcCgmmocMx4S1w2lAaxXtckxxeDtcVNtkOISUWm/mbC+BrmYPXoGMkfhkKOQ== + dependencies: + debug "^4.3.1" + events "^3.2.0" + retry "^0.12.0" + optionalDependencies: + websocket "^1.0.33" + +protoo-server@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/protoo-server/-/protoo-server-4.0.6.tgz#58d877a00417f4e135e1525250d2223800050c7d" + integrity sha512-y5Ix5r0D6oCaajHJJisVrzRYPoG9IA5yxLQIhlnrWdhg89G5KEsDgTUNn4495cxShj9aPIl0Lxs5Sa3L5ElgGQ== + dependencies: + debug "^4.3.1" + websocket "^1.0.33" + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +pure-rand@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" + integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== + +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + +qs@^6.11.0: + version "6.14.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930" + integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w== + dependencies: + side-channel "^1.1.0" + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +react-is@^18.0.0: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" + integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== + +readable-stream@^2.2.2: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.4.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" + integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w== + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" + integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A== + dependencies: + redis-errors "^1.0.0" + +reflect-metadata@^0.1.13: + version "0.1.14" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.14.tgz#24cf721fe60677146bb77eeb0e1f9dece3d65859" + integrity sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A== + +repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve.exports@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.3.tgz#41955e6f1b4013b7586f873749a635dea07ebe3f" + integrity sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A== + +resolve@^1.20.0: + version "1.22.10" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39" + integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== + dependencies: + is-core-module "^2.16.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rimraf@^5.0.5: + version "5.0.10" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.10.tgz#23b9843d3dc92db71f96e1a2ce92e39fd2a8221c" + integrity sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ== + dependencies: + glob "^10.3.7" + +run-async@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== + +run-async@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-3.0.0.tgz#42a432f6d76c689522058984384df28be379daad" + integrity sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q== + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +rxjs@7.8.1, rxjs@^7.5.5, rxjs@^7.8.1: + version "7.8.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== + dependencies: + tslib "^2.1.0" + +safe-buffer@5.2.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +schema-utils@^3.1.1, schema-utils@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +schema-utils@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.0.tgz#3b669f04f71ff2dfb5aba7ce2d5a9d79b35622c0" + integrity sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + +selfsigned@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" + integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== + dependencies: + "@types/node-forge" "^1.3.0" + node-forge "^1" + +semver@^6.3.0, semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.3.4, semver@^7.3.5, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.6.3: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-javascript@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== + dependencies: + encodeurl "~2.0.0" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.19.0" + +set-function-length@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + 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" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + 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" + +side-channel@^1.0.6, side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + 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" + +signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +signal-exit@^4.0.1, signal-exit@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +source-map-support@0.5.13: + version "0.5.13" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-support@^0.5.21, source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@0.7.4, source-map@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" + integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== + +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +stack-utils@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== + dependencies: + escape-string-regexp "^2.0.0" + +standard-as-callback@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" + integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + +string-length@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + +"string-width-cjs@npm:string-width@^4.2.0", string-width@4, string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^5.0.0, strip-ansi@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +superagent@^8.1.2: + version "8.1.2" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.1.2.tgz#03cb7da3ec8b32472c9d20f6c2a57c7f3765f30b" + integrity sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA== + dependencies: + component-emitter "^1.3.0" + cookiejar "^2.1.4" + debug "^4.3.4" + fast-safe-stringify "^2.1.1" + form-data "^4.0.0" + formidable "^2.1.2" + methods "^1.1.2" + mime "2.6.0" + qs "^6.11.0" + semver "^7.3.8" + +supertest@^6.3.3: + version "6.3.4" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.3.4.tgz#2145c250570c2ea5d337db3552dbfb78a2286218" + integrity sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw== + dependencies: + methods "^1.1.2" + superagent "^8.1.2" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-color@^9.4.0: + version "9.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-9.4.0.tgz#17bfcf686288f531db3dea3215510621ccb55954" + integrity sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw== + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +swagger-ui-dist@5.18.2: + version "5.18.2" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-5.18.2.tgz#62013074374d272c04ed3030704b88db5aa8c0b7" + integrity sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw== + dependencies: + "@scarf/scarf" "=1.4.0" + +symbol-observable@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205" + integrity sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ== + +synckit@^0.9.1: + version "0.9.2" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.9.2.tgz#a3a935eca7922d48b9e7d6c61822ee6c3ae4ec62" + integrity sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw== + dependencies: + "@pkgr/core" "^0.1.0" + tslib "^2.6.2" + +tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +tar@^7.4.3: + version "7.4.3" + resolved "https://registry.yarnpkg.com/tar/-/tar-7.4.3.tgz#88bbe9286a3fcd900e94592cda7a22b192e80571" + integrity sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw== + dependencies: + "@isaacs/fs-minipass" "^4.0.0" + chownr "^3.0.0" + minipass "^7.1.2" + minizlib "^3.0.1" + mkdirp "^3.0.1" + yallist "^5.0.0" + +terser-webpack-plugin@^5.3.10: + version "5.3.11" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz#93c21f44ca86634257cac176f884f942b7ba3832" + integrity sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ== + dependencies: + "@jridgewell/trace-mapping" "^0.3.25" + jest-worker "^27.4.5" + schema-utils "^4.3.0" + serialize-javascript "^6.0.2" + terser "^5.31.1" + +terser@^5.31.1: + version "5.37.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.37.0.tgz#38aa66d1cfc43d0638fab54e43ff8a4f72a21ba3" + integrity sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +tmpl@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +tree-kill@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" + integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== + +ts-api-utils@^1.0.1: + version "1.4.3" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz#bfc2215fe6528fecab2b0fba570a2e8a4263b064" + integrity sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw== + +ts-jest@^29.1.0: + version "29.2.5" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.2.5.tgz#591a3c108e1f5ebd013d3152142cb5472b399d63" + integrity sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA== + dependencies: + bs-logger "^0.2.6" + ejs "^3.1.10" + fast-json-stable-stringify "^2.1.0" + jest-util "^29.0.0" + json5 "^2.2.3" + lodash.memoize "^4.1.2" + make-error "^1.3.6" + semver "^7.6.3" + yargs-parser "^21.1.1" + +ts-loader@^9.4.3: + version "9.5.2" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.5.2.tgz#1f3d7f4bb709b487aaa260e8f19b301635d08020" + integrity sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^5.0.0" + micromatch "^4.0.0" + semver "^7.3.4" + source-map "^0.7.4" + +ts-node@^10.9.1: + version "10.9.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + 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" + +tsconfig-paths-webpack-plugin@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz#f7459a8ed1dd4cf66ad787aefc3d37fff3cf07fc" + integrity sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^5.7.0" + tapable "^2.2.1" + tsconfig-paths "^4.1.2" + +tsconfig-paths@4.2.0, tsconfig-paths@^4.1.2, tsconfig-paths@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" + integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== + dependencies: + json5 "^2.2.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tslib@2.8.1, tslib@^2.1.0, tslib@^2.6.2: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-detect@4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-is@^1.6.4, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +type@^2.7.2: + version "2.7.3" + resolved "https://registry.yarnpkg.com/type/-/type-2.7.3.tgz#436981652129285cc3ba94f392886c2637ea0486" + integrity sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ== + +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + +typescript@5.7.2: + version "5.7.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6" + integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg== + +typescript@^5.1.3: + version "5.7.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.3.tgz#919b44a7dbb8583a9b856d162be24a54bf80073e" + integrity sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw== + +uid@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/uid/-/uid-2.0.2.tgz#4b5782abf0f2feeefc00fa88006b2b3b7af3e3b9" + integrity sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g== + dependencies: + "@lukeed/csprng" "^1.0.0" + +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== + +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz#97e9c96ab0ae7bcac08e9ae5151d26e6bc6b5580" + integrity sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +utf-8-validate@^5.0.2: + version "5.0.10" + resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2" + integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ== + dependencies: + node-gyp-build "^4.3.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^11.0.3: + version "11.0.5" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.0.5.tgz#07b46bdfa6310c92c3fb3953a8720f170427fc62" + integrity sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +v8-to-istanbul@^9.0.1: + version "9.3.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz#b9572abfa62bd556c16d75fdebc1a411d5ff3175" + integrity sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.12" + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^2.0.0" + +validator@^13.9.0: + version "13.12.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.12.0.tgz#7d78e76ba85504da3fee4fd1922b385914d4b35f" + integrity sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg== + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +walker@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== + dependencies: + makeerror "1.0.12" + +watchpack@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" + integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wcwidth@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== + dependencies: + defaults "^1.0.3" + +web-streams-polyfill@^3.0.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" + integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +webpack-node-externals@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz#1a3407c158d547a9feb4229a9e3385b7b60c9917" + integrity sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ== + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@5.97.1: + version "5.97.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.97.1.tgz#972a8320a438b56ff0f1d94ade9e82eac155fa58" + integrity sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg== + 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" + +websocket@^1.0.33: + version "1.0.35" + resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.35.tgz#374197207d7d4cc4c36cbf8a1bb886ee52a07885" + integrity sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q== + dependencies: + bufferutil "^4.0.1" + debug "^2.2.0" + es5-ext "^0.10.63" + typedarray-to-buffer "^3.1.5" + utf-8-validate "^5.0.2" + yaeti "^0.0.6" + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +widest-line@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" + integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== + dependencies: + string-width "^4.0.0" + +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@5: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" + integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== + dependencies: + ansi-styles "^3.2.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + +wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +write-file-atomic@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" + integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yaeti@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577" + integrity sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-5.0.0.tgz#00e2de443639ed0d78fd87de0d27469fbcffb533" + integrity sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw== + +yargs-parser@21.1.1, yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^17.3.1: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + 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" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +yoctocolors-cjs@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz#f4b905a840a37506813a7acaa28febe97767a242" + integrity sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA== diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..e5b549d --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,17 @@ +{ + "include-component-in-tag": true, + "include-v-in-tag": true, + "tag-separator": "@", + "separate-pull-requests": true, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "packages": { + "packages/loadbalancer": { + "release-type": "node" + }, + "packages/webrtc-server": { + "release-type": "node" + } + }, + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" +} diff --git a/tsconfig.build.json b/tsconfig.build.json index 64f86c6..0dcde15 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,4 +1,27 @@ { - "extends": "./tsconfig.json", - "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] + "compilerOptions": { + "module": "commonjs", + "target": "ES2017", + "declaration": true, + "sourceMap": true, + "strict": true, + "noEmitOnError": true, + "lib": [], + "types": [], + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "skipLibCheck": true + }, + "exclude": [ + "node_modules", + "build", + "**/*.test.ts", + "**/__tests__/*.ts", + "**/__mocks__/*.ts", + "**/build/**", + "scripts" + ] } diff --git a/tsconfig.json b/tsconfig.json index b3bf1a3..62e90ab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,22 +1,12 @@ { + "extends": "./tsconfig.build.json", + "ts-node": { + "require": ["tsconfig-paths/register"], + "files": true + }, "compilerOptions": { - "module": "commonjs", - "declaration": true, - "removeComments": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "target": "ES2021", - "sourceMap": true, - "outDir": "./dist", - "baseUrl": "./", - "incremental": true, - "skipLibCheck": true, - "strictNullChecks": false, - "noImplicitAny": false, - "strictBindCallApply": false, - "forceConsistentCasingInFileNames": false, - "noFallthroughCasesInSwitch": false, - "types": ["node", "jest", "@babel/core"] - } + "baseUrl": ".", + "types": ["jest", "node"] + }, + "exclude": ["node_modules", "build"] } diff --git a/yarn.lock b/yarn.lock index f01b1bd..4d32646 100644 --- a/yarn.lock +++ b/yarn.lock @@ -813,6 +813,36 @@ resolved "https://registry.yarnpkg.com/@nestjs/axios/-/axios-3.1.3.tgz#cf73f317f89800ec2f6f04b577677617c5aef0d9" integrity sha512-RZ/63c1tMxGLqyG3iOCVt7A72oy4x1eM6QEhd4KzCYpaVWW0igq0WSREeRoEZhIxRcZfDfIIkvsOMiM7yfVGZQ== +"@nestjs/axios@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@nestjs/axios/-/axios-4.0.0.tgz#520ff7c6238e635658fe334ee62db9d1b9c5546f" + integrity sha512-1cB+Jyltu/uUPNQrpUimRHEQHrnQrpLzVj6dU3dgn6iDDDdahr10TgHFGTmw5VuJ9GzKZsCLDL78VSwJAs/9JQ== + +"@nestjs/cli@^10.0.0": + version "10.4.9" + resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-10.4.9.tgz#ac3a23096a4725465360d8d60810f3e857f4a803" + integrity sha512-s8qYd97bggqeK7Op3iD49X2MpFtW4LVNLAwXFkfbRxKME6IYT7X0muNTJ2+QfI8hpbNx9isWkrLWIp+g5FOhiA== + 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" + "@nestjs/cli@^10.4.8": version "10.4.8" resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-10.4.8.tgz#e6dec4eeda8a125918cbf1c0d10773ac6bb6d40e" @@ -856,6 +886,15 @@ dotenv-expand "10.0.0" lodash "4.17.21" +"@nestjs/config@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@nestjs/config/-/config-4.0.0.tgz#b560fbd07e8083bc3c8d66afdb2a98f5882c5853" + integrity sha512-hyhUMtVwlT+tavtPNyekl8iP0QTU1U6awKrgdOSxhMhp3TQMltx7hz2yqGTcARp+19zWPfgJudyxthuD3lPp/Q== + dependencies: + dotenv "16.4.7" + dotenv-expand "12.0.1" + lodash "4.17.21" + "@nestjs/core@^10.0.0": version "10.4.13" resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-10.4.13.tgz#82c9c33ba1d9042fb7a6d26054d0e7d4a5d564c6" @@ -915,7 +954,7 @@ boxen "5.1.2" check-disk-space "3.4.0" -"@nestjs/testing@^10.4.15": +"@nestjs/testing@^10.0.0", "@nestjs/testing@^10.4.15": version "10.4.15" resolved "https://registry.yarnpkg.com/@nestjs/testing/-/testing-10.4.15.tgz#4c9fe17bf026c0142040cbe3db464c526f89d36a" integrity sha512-eGlWESkACMKti+iZk1hs6FUY/UqObmMaa8HAN9JLnaYkoLf1Jeh+EuHlGnfqo/Rq77oznNLIyaA3PFjrFDlNUg== @@ -1150,7 +1189,7 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@^29.5.14": +"@types/jest@^29.5.14", "@types/jest@^29.5.2": version "29.5.14" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.14.tgz#2b910912fa1d6856cadcd0c1f95af7df1d6049e5" integrity sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ== @@ -1192,6 +1231,13 @@ dependencies: undici-types "~6.20.0" +"@types/node@^20.3.1": + version "20.17.16" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.17.16.tgz#b33b0edc1bf925b27349e494b871ca4451fabab4" + integrity sha512-vOTpLduLkZXePLxHiHsBLp98mHGnl8RptV4YAO3HfKO5UHjDvySGbxKtpYfy8Sx5+WKcgc45qNreJJRVM3L6mw== + dependencies: + undici-types "~6.19.2" + "@types/protoo-server@^4.0.6": version "4.0.6" resolved "https://registry.yarnpkg.com/@types/protoo-server/-/protoo-server-4.0.6.tgz#3106d43ff1d3ef9cdd3726de191ee552029b7c4e" @@ -1370,7 +1416,7 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.12.1": +"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.12.1", "@webassemblyjs/ast@^1.14.1": version "1.14.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6" integrity sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ== @@ -1436,7 +1482,7 @@ resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.13.2.tgz#917a20e93f71ad5602966c2d685ae0c6c21f60f1" integrity sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ== -"@webassemblyjs/wasm-edit@^1.12.1": +"@webassemblyjs/wasm-edit@^1.12.1", "@webassemblyjs/wasm-edit@^1.14.1": version "1.14.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz#ac6689f502219b59198ddec42dcd496b1004d597" integrity sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ== @@ -1471,7 +1517,7 @@ "@webassemblyjs/wasm-gen" "1.14.1" "@webassemblyjs/wasm-parser" "1.14.1" -"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.12.1": +"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.12.1", "@webassemblyjs/wasm-parser@^1.14.1": version "1.14.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz#b3e13f1893605ca78b52c68e54cf6a865f90b9fb" integrity sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ== @@ -2344,11 +2390,23 @@ dotenv-expand@10.0.0: resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-10.0.0.tgz#12605d00fb0af6d0a592e6558585784032e4ef37" integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A== +dotenv-expand@12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-12.0.1.tgz#44bdfa204a368100689ec35d7385755f599ceeb1" + integrity sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ== + dependencies: + dotenv "^16.4.5" + dotenv@16.4.5: version "16.4.5" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== +dotenv@16.4.7, dotenv@^16.4.5: + version "16.4.7" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.7.tgz#0e20c5b82950140aa99be360a8a5f52335f53c26" + integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ== + eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" @@ -3787,7 +3845,7 @@ jest-worker@^29.7.0: merge-stream "^2.0.0" supports-color "^8.0.0" -jest@^29.7.0: +jest@^29.5.0, jest@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest/-/jest-29.7.0.tgz#994676fc24177f088f1c5e3737f5697204ff2613" integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== @@ -5301,7 +5359,7 @@ typescript@5.6.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b" integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw== -typescript@^5.1.3: +typescript@5.7.2, typescript@^5.1.3: version "5.7.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6" integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg== @@ -5313,6 +5371,11 @@ uid@2.0.2: dependencies: "@lukeed/csprng" "^1.0.0" +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + undici-types@~6.20.0: version "6.20.0" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" @@ -5460,6 +5523,35 @@ webpack@5.96.1: watchpack "^2.4.1" webpack-sources "^3.2.3" +webpack@5.97.1: + version "5.97.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.97.1.tgz#972a8320a438b56ff0f1d94ade9e82eac155fa58" + integrity sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg== + 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" + websocket@^1.0.33: version "1.0.35" resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.35.tgz#374197207d7d4cc4c36cbf8a1bb886ee52a07885" From 8b4d093b7c1fe3bc6d697ff257048e974fea3199 Mon Sep 17 00:00:00 2001 From: Gabriel Mata Date: Mon, 27 Jan 2025 17:54:26 -0500 Subject: [PATCH 02/15] feat: add loadbalancing function and dockers file to test --- packages/loadbalancer/Dockerfile | 26 ++ packages/loadbalancer/docker-compose.yml | 32 +++ packages/loadbalancer/package.json | 1 + packages/loadbalancer/src/dto/rooms.dto.ts | 4 +- .../src/loadbalancer.controller.ts | 96 +++++--- .../loadbalancer/src/loadbalancer.service.ts | 222 +++++++++++++----- packages/loadbalancer/src/main.ts | 2 +- 7 files changed, 294 insertions(+), 89 deletions(-) create mode 100644 packages/loadbalancer/Dockerfile create mode 100644 packages/loadbalancer/docker-compose.yml diff --git a/packages/loadbalancer/Dockerfile b/packages/loadbalancer/Dockerfile new file mode 100644 index 0000000..d2091b3 --- /dev/null +++ b/packages/loadbalancer/Dockerfile @@ -0,0 +1,26 @@ +FROM node:22-alpine3.20 AS base + +# MPR Setup +WORKDIR /app + +# Copy package.json and yarn.lock first to leverage Docker layer caching +COPY package.json package.json + +# FIXME: yarn.lock per workspace +#COPY yarn.lock yarn.lock + +# Run yarn install after copying only dependency files +RUN yarn install + +# Copy other dependencies and configuration files +COPY ./src ./src +COPY tsconfig.json tsconfig.json +COPY tsconfig.build.json tsconfig.build.json + +# Build the project +RUN yarn build + +VOLUME /app/dist/config + +# Command to run when the container starts +CMD ["yarn", "start"] diff --git a/packages/loadbalancer/docker-compose.yml b/packages/loadbalancer/docker-compose.yml new file mode 100644 index 0000000..77110e3 --- /dev/null +++ b/packages/loadbalancer/docker-compose.yml @@ -0,0 +1,32 @@ +version: '3.9' + +services: + loadbalancer-webrtc: + build: . + image: loadbalancer-webrtc:test + container_name: loadbalancer-webrtc + environment: + - LOG_LEVEL=3 + - APP_PORT=3000 + - REDIS_URL=redis://redis:6379 + restart: always + ports: + - 3000:3000 + networks: + - loadbalancing + depends_on: + - redis + + redis: + container_name: redis + image: redis:alpine + restart: always + ports: + - 6379:6379 + networks: + - loadbalancing + command: redis-server --maxmemory 64mb --maxmemory-policy allkeys-lru + +networks: + loadbalancing: + external: true diff --git a/packages/loadbalancer/package.json b/packages/loadbalancer/package.json index 343975e..353902c 100644 --- a/packages/loadbalancer/package.json +++ b/packages/loadbalancer/package.json @@ -29,6 +29,7 @@ "@nestjs/swagger": "^8.1.0", "axios": "^1.7.8", "class-validator": "^0.14.1", + "class-transformer": "^0.5.1", "ioredis": "^5.4.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1" diff --git a/packages/loadbalancer/src/dto/rooms.dto.ts b/packages/loadbalancer/src/dto/rooms.dto.ts index 9838ab0..d458d0d 100644 --- a/packages/loadbalancer/src/dto/rooms.dto.ts +++ b/packages/loadbalancer/src/dto/rooms.dto.ts @@ -77,7 +77,7 @@ export class RoomClosedDto { export interface ServerData { serverId: string url: string - capacity: number + workers: number } /** @@ -85,6 +85,8 @@ export interface ServerData { */ export interface AvailableServer extends ServerData { load: number + capacity: number + consumers: number } /** diff --git a/packages/loadbalancer/src/loadbalancer.controller.ts b/packages/loadbalancer/src/loadbalancer.controller.ts index 0d72768..1046d37 100644 --- a/packages/loadbalancer/src/loadbalancer.controller.ts +++ b/packages/loadbalancer/src/loadbalancer.controller.ts @@ -1,22 +1,24 @@ -import { Body, Controller, Get, HttpException, HttpStatus, Logger, Param, Post } from '@nestjs/common' +import { Body, Controller, Post, HttpException, HttpStatus, Logger, Param } from '@nestjs/common' import { LoadbalancerService } from './loadbalancer.service' -import { ApiBody, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger' -import { CreateRoomDto } from './dto/rooms.dto' +import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger' +import { CreateRoomDto, ServerData } from './dto/rooms.dto' +@ApiTags('Load Balancer') @Controller() export class LoadbalancerController { private readonly logger = new Logger(LoadbalancerController.name) + constructor(private readonly loadbalancerService: LoadbalancerService) {} /** * Endpoint to create or retrieve a room. - * Delegates the logic to the RoomsService. - * @param roomId - The ID of the room (optional). - * @param createRoomDto - Parameters for room creation (optional). + * Delegates the logic to the LoadbalancerService. + * @param {string} roomId - The ID of the room (optional). + * @param {CreateRoomDto} createRoomDto - Parameters for room creation. * @returns {object} - Room details and WebSocket URL. */ @Post(':roomId?') - @ApiOperation({ summary: 'Create room' }) + @ApiOperation({ summary: 'Create or retrieve a room' }) @ApiParam({ name: 'roomId', description: 'Room identifier (optional)', @@ -28,14 +30,8 @@ export class LoadbalancerController { schema: { type: 'object', properties: { - eventNotificationUri: { - type: 'string', - example: 'http://example.com/notification', - }, - maxPeerCount: { - type: 'number', - example: 3, - }, + eventNotificationUri: { type: 'string', example: 'http://example.com/notification' }, + maxPeerCount: { type: 'number', example: 3 }, }, }, }) @@ -65,17 +61,37 @@ export class LoadbalancerController { } /** - * Endpoint para registrar un servidor en el balanceador de carga. - * @param {object} serverData - Información del servidor. - * @param {string} serverData.serverId - ID único del servidor. - * @param {string} serverData.url - URL base del servidor. - * @param {number} serverData.capacity - Capacidad máxima del servidor. + * Endpoint to register a server with the load balancer. + * Registers the server and calculates its capacity based on the workers provided. + * @param {ServerData} serverData - Server information. + * @returns {Promise} - A success message indicating the server was registered. */ @Post('register') - async registerServer( - @Body() serverData: { serverId: string; url: string; capacity: number }, - ): Promise<{ message: string }> { - if (!serverData.serverId || !serverData.url || !serverData.capacity) { + @ApiOperation({ summary: 'Register a server' }) + @ApiBody({ + description: 'Server registration data', + schema: { + type: 'object', + properties: { + serverId: { type: 'string', example: 'server-12345' }, + url: { type: 'string', example: 'http://example.com' }, + workers: { type: 'number', example: 4 }, + }, + }, + }) + @ApiResponse({ + status: 201, + description: 'Server registered successfully.', + schema: { + example: { message: 'Server registered successfully' }, + }, + }) + @ApiResponse({ + status: 400, + description: 'Invalid server data.', + }) + async registerServer(@Body() serverData: ServerData): Promise<{ message: string }> { + if (!serverData.serverId || !serverData.url || !serverData.workers) { throw new HttpException('Invalid server data', HttpStatus.BAD_REQUEST) } @@ -84,12 +100,36 @@ export class LoadbalancerController { } /** - * Endpoint para notificar el cierre de una sala en un servidor. - * @param {object} roomData - Información de la sala. - * @param {string} roomData.serverId - ID del servidor donde se cerró la sala. - * @param {string} roomData.roomId - ID de la sala que se cerró. + * Endpoint to notify the closure of a room on a server. + * Updates the server load and removes the room from Redis. + * @param {object} roomData - Room information. + * @param {string} roomData.serverId - The ID of the server where the room was closed. + * @param {string} roomData.roomId - The ID of the room that was closed. + * @returns {Promise} - A success message indicating the notification was processed. */ @Post('room-closed') + @ApiOperation({ summary: 'Notify room closure' }) + @ApiBody({ + description: 'Room closure data', + schema: { + type: 'object', + properties: { + serverId: { type: 'string', example: 'server-12345' }, + roomId: { type: 'string', example: 'room-67890' }, + }, + }, + }) + @ApiResponse({ + status: 201, + description: 'Room closed notification processed successfully.', + schema: { + example: { message: 'Room closed notification processed successfully' }, + }, + }) + @ApiResponse({ + status: 404, + description: 'Server or room not found.', + }) async notifyRoomClosed(@Body() roomData: { serverId: string; roomId: string }): Promise<{ message: string }> { await this.loadbalancerService.roomClosed(roomData) return { message: 'Room closed notification processed successfully' } diff --git a/packages/loadbalancer/src/loadbalancer.service.ts b/packages/loadbalancer/src/loadbalancer.service.ts index b27c8f1..a8d650c 100644 --- a/packages/loadbalancer/src/loadbalancer.service.ts +++ b/packages/loadbalancer/src/loadbalancer.service.ts @@ -1,9 +1,8 @@ import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common' import { HttpRequestService } from './lib/HttpRequestService' -import { ApiResponse } from '@nestjs/swagger' import Redis from 'ioredis' import { InjectRedis } from '@nestjs-modules/ioredis' -import { AvailableServer, RoomData, RoomResponse } from './dto/rooms.dto' +import { AvailableServer, RoomData, RoomResponse, ServerData } from './dto/rooms.dto' @Injectable() export class LoadbalancerService { @@ -55,8 +54,9 @@ export class LoadbalancerService { this.logger.log( `[createRoom] Room created successfully on server ${bestServer.url}. Response: ${JSON.stringify(response)}`, ) - // increment incrementRoomCount - this.incrementRoomCount(bestServer.serverId) + // Update the server load + await this.updateLoadServer(bestServer.serverId, roomId, maxPeerCount || 2) + // Return the response containing room details return response } catch (error) { @@ -66,66 +66,56 @@ export class LoadbalancerService { } /** - * Registers a server in Redis. - * This method stores server information in Redis for load balancing. - * - * @param {object} serverData - The server data to register. - * @param {string} serverData.serverId - Unique identifier for the server. - * @param {string} serverData.url - Base URL of the server. - * @param {number} serverData.capacity - Maximum capacity of the server. - * @returns {Promise} - Resolves when the server is successfully registered. - */ - public async registerServer(serverData: { serverId: string; url: string; capacity: number }): Promise { - try { - const key = `server:${serverData.serverId}` - await this.redisClient.hset(key, 'url', serverData.url, 'capacity', serverData.capacity.toString(), 'load', '0') - this.logger.log(`[registerServer] Server registered successfully: ${JSON.stringify(serverData)}`) - } catch (error) { - this.logger.error(`[registerServer] Failed to register server: ${error.message}`) - throw new HttpException({ error: error.message }, HttpStatus.INTERNAL_SERVER_ERROR) - } - } - - /** - * Increments the number of active rooms for a server. + * Updates the server load by registering or modifying a room. + * Reduces the server's available capacity based on the maxPeerCount. * @param {string} serverId - The ID of the server. + * @param {string} roomId - The ID of the room. + * @param {number} maxPeerCount - The maximum number of peers in the room. */ - public async incrementRoomCount(serverId: string): Promise { - const key = `server:${serverId}` - const exists = await this.redisClient.exists(key) - if (!exists) { - this.logger.warn(`[incrementRoomCount] Server ${serverId} not found in Redis.`) - return + public async updateLoadServer(serverId: string, roomId: string, maxPeerCount: number): Promise { + const roomKey = `room:${serverId}:${roomId}` + const serverKey = `server:${serverId}` + + // Validate maxPeerCount + if (maxPeerCount <= 0) { + this.logger.error(`[updateLoadServer] Invalid maxPeerCount (${maxPeerCount}) for room ${roomId}.`) + throw new HttpException('Invalid maxPeerCount', HttpStatus.BAD_REQUEST) } - await this.redisClient.hincrby(key, 'rooms', 1) - this.logger.log(`[incrementRoomCount] Incremented room count for server ${serverId}.`) - } - /** - * Decrements the number of active rooms for a server. - * @param {RoomData} roomData - The data of the room that was closed. - */ - public async roomClosed(roomData: RoomData): Promise { - const key = `server:${roomData.serverId}` - const exists = await this.redisClient.exists(key) + // Calculate total consumers for the room + const totalConsumers = maxPeerCount * (maxPeerCount - 1) * 2 + + // Check if server exists + const exists = await this.redisClient.exists(serverKey) if (!exists) { - this.logger.warn(`[roomClosed] Server ${roomData.serverId} not found in Redis.`) - return + this.logger.error(`[updateLoadServer] Server ${serverId} not found.`) + throw new HttpException('Server not found', HttpStatus.NOT_FOUND) } - const currentRooms = await this.redisClient.hget(key, 'rooms') - if (Number(currentRooms) > 0) { - await this.redisClient.hincrby(key, 'rooms', -1) - this.logger.log( - `[roomClosed] Decremented room count for server ${roomData.serverId}. Room ID: ${roomData.roomId}`, + // Get current capacity + const currentCapacity = await this.redisClient.hget(serverKey, 'capacity') + if (!currentCapacity || Number(currentCapacity) < totalConsumers) { + this.logger.error( + `[updateLoadServer] Insufficient capacity on server ${serverId} for room ${roomId}. Required: ${totalConsumers}, Available: ${currentCapacity}`, ) - } else { - this.logger.warn(`[roomClosed] Room count for server ${roomData.serverId} is already 0.`) + throw new HttpException('Insufficient server capacity', HttpStatus.BAD_REQUEST) } + + // Deduct capacity and increment room count + await this.redisClient.hincrby(serverKey, 'capacity', -totalConsumers) + await this.redisClient.hincrby(serverKey, 'rooms', 1) + + // Register room details + await this.redisClient.hset(roomKey, 'peers', maxPeerCount.toString()) + + this.logger.log( + `[updateLoadServer] Room ${roomId} updated on server ${serverId} with ${maxPeerCount} peers. Consumers: ${totalConsumers}`, + ) } /** * Retrieves the list of available servers sorted by load. + * Load is calculated as the used capacity divided by the maximum capacity. * * @returns {Promise} - List of servers with their load and capacity. * @throws {HttpException} - Throws if Redis fails to retrieve server information. @@ -134,20 +124,30 @@ export class LoadbalancerService { try { this.logger.log('[getAvailableServers] Fetching available servers from Redis.') const keys = await this.redisClient.keys('server:*') - const servers = [] + const servers: AvailableServer[] = [] for (const key of keys) { const serverData = await this.redisClient.hgetall(key) + + // Calculate total consumers across all rooms for this server + const totalConsumers = await this.calculateConsumersForServer(serverData.serverId) + + const capacity = Number(serverData.capacity) + const load = totalConsumers / capacity + const workers = Number(serverData.workers) + servers.push({ serverId: key.split(':')[1], - ...serverData, - capacity: Number(serverData.capacity), - load: Number(serverData.load), + workers, + url: serverData.url, + capacity, + load, + consumers: totalConsumers, }) } - // Sort servers by load (load/capacity ratio) - return servers.sort((a, b) => a.load / a.capacity - b.load / b.capacity) + // Sort servers by load (lower load first) + return servers.sort((a, b) => a.load - b.load) } catch (error) { this.logger.error(`[getAvailableServers] Failed to retrieve available servers: ${error.message}`) throw new HttpException({ error: error.message }, HttpStatus.INTERNAL_SERVER_ERROR) @@ -165,10 +165,10 @@ export class LoadbalancerService { const servers = await this.getAvailableServers() if (servers.length === 0) { this.logger.warn('[getBestServer] No servers available.') - throw new Error('[getBestServer] No servers available') + throw new Error('[getBestServer] No servers available.') } - const bestServer = servers[0] + const bestServer = servers[0] // Select the server with the lowest load this.logger.log(`[getBestServer] Best server selected: ${JSON.stringify(bestServer)}`) return bestServer } catch (error) { @@ -176,4 +176,108 @@ export class LoadbalancerService { throw new HttpException({ error: error.message }, HttpStatus.INTERNAL_SERVER_ERROR) } } + + /** + * Calculates the total number of consumers for all rooms on a given server. + * @param {string} serverId - The ID of the server. + * @returns {Promise} - Total number of consumers. + */ + private async calculateConsumersForServer(serverId: string): Promise { + const roomKeys = await this.redisClient.keys(`room:${serverId}:*`) + let totalConsumers = 0 + + for (const roomKey of roomKeys) { + const peerCount = await this.redisClient.hget(roomKey, 'peers') + if (peerCount) { + const peers = Number(peerCount) + totalConsumers += peers * (peers - 1) * 2 + } + } + + return totalConsumers + } + + /** + * Registers a WebRTC server in Redis. + * Calculates the server's capacity based on the number of workers and stores it in Redis. + * + * @param {ServerData} serverData - The server data to register. + * @throws {HttpException} - If the server data is invalid. + */ + public async registerServer(serverData: ServerData): Promise { + const { serverId, url, workers } = serverData + + // Validate input + if (!serverId || !url || !workers || workers <= 0) { + this.logger.error('Invalid server data provided for registration.') + throw new HttpException('Invalid server data', HttpStatus.BAD_REQUEST) + } + + // Calculate maximum capacity based on the number of workers + const capacity = workers * 500 // 500 consumers per worker + + const key = `server:${serverId}` + await this.redisClient.hset( + key, + 'url', + url, + 'capacity', + capacity.toString(), + 'rooms', + '0', // Initial number of active rooms is 0 + ) + + this.logger.log(`Server registered: ${JSON.stringify({ serverId, url, workers, capacity })}`) + } + + /** + * Updates the server load and capacity when a room is closed. + * @param {RoomData} roomData - The data of the room that was closed. + */ + public async roomClosed(roomData: RoomData): Promise { + const serverKey = `server:${roomData.serverId}` + const roomKey = `room:${roomData.serverId}:${roomData.roomId}` + + // Check if the server exists + const serverExists = await this.redisClient.exists(serverKey) + if (!serverExists) { + this.logger.warn(`[roomClosed] Server ${roomData.serverId} not found in Redis.`) + return + } + + // Check if the room exists + const roomExists = await this.redisClient.exists(roomKey) + if (!roomExists) { + this.logger.warn(`[roomClosed] Room ${roomData.roomId} not found on server ${roomData.serverId}.`) + return + } + + // Retrieve the number of peers in the room + const peerCount = await this.redisClient.hget(roomKey, 'peers') + if (!peerCount) { + this.logger.warn(`[roomClosed] Peer count not found for room ${roomData.roomId}.`) + return + } + + // Calculate the total consumers for the room + const totalConsumers = Number(peerCount) * (Number(peerCount) - 1) * 2 + + // Increment the server capacity + await this.redisClient.hincrby(serverKey, 'capacity', totalConsumers) + + // Decrement the room count + const currentRooms = await this.redisClient.hget(serverKey, 'rooms') + if (Number(currentRooms) > 0) { + await this.redisClient.hincrby(serverKey, 'rooms', -1) + this.logger.log( + `[roomClosed] Room ${roomData.roomId} closed on server ${roomData.serverId}. Consumers freed: ${totalConsumers}`, + ) + } else { + this.logger.warn(`[roomClosed] Room count for server ${roomData.serverId} is already 0.`) + } + + // Remove room data from Redis + await this.redisClient.del(roomKey) + this.logger.log(`[roomClosed] Room ${roomData.roomId} removed from Redis.`) + } } diff --git a/packages/loadbalancer/src/main.ts b/packages/loadbalancer/src/main.ts index 6a556b6..2cc0891 100644 --- a/packages/loadbalancer/src/main.ts +++ b/packages/loadbalancer/src/main.ts @@ -45,7 +45,7 @@ async function bootstrap() { const configService = app.get(ConfigService) - const PORT = configService.get('appConfig.port') + const PORT = configService.get('appConfig.appPort') // Start the server app.listen(PORT, () => { From 3106905306ecafb52bdba8b3ec54daf687a4e1da Mon Sep 17 00:00:00 2001 From: Gabriel Mata Date: Tue, 28 Jan 2025 17:12:13 -0500 Subject: [PATCH 03/15] feat: Add serverHealt function to servers and fix on services about loadbalancer --- packages/loadbalancer/docker-compose.yml | 1 + packages/loadbalancer/package.json | 2 +- .../loadbalancer/src/config/app.config.ts | 8 ++ .../loadbalancer/src/config/logger.config.ts | 3 +- packages/loadbalancer/src/dto/rooms.dto.ts | 2 - .../src/lib/HttpRequestService.ts | 4 +- .../src/lib/ServerHealthChecker.ts | 94 +++++++++++++++++++ .../src/loadbalancer.controller.ts | 3 +- .../loadbalancer/src/loadbalancer.module.ts | 3 +- .../loadbalancer/src/loadbalancer.service.ts | 93 +++++++++--------- 10 files changed, 162 insertions(+), 51 deletions(-) create mode 100644 packages/loadbalancer/src/lib/ServerHealthChecker.ts diff --git a/packages/loadbalancer/docker-compose.yml b/packages/loadbalancer/docker-compose.yml index 77110e3..a6926dd 100644 --- a/packages/loadbalancer/docker-compose.yml +++ b/packages/loadbalancer/docker-compose.yml @@ -9,6 +9,7 @@ services: - LOG_LEVEL=3 - APP_PORT=3000 - REDIS_URL=redis://redis:6379 + - HEALTH_CHECK_INTERVAL=20000 restart: always ports: - 3000:3000 diff --git a/packages/loadbalancer/package.json b/packages/loadbalancer/package.json index 353902c..9a6d321 100644 --- a/packages/loadbalancer/package.json +++ b/packages/loadbalancer/package.json @@ -4,7 +4,7 @@ "description": "", "author": "", "private": true, - "license": "UNLICENSED", + "license": "Apache-2.0", "scripts": { "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", diff --git a/packages/loadbalancer/src/config/app.config.ts b/packages/loadbalancer/src/config/app.config.ts index 4a52b0c..329cc6b 100644 --- a/packages/loadbalancer/src/config/app.config.ts +++ b/packages/loadbalancer/src/config/app.config.ts @@ -40,4 +40,12 @@ export default registerAs('appConfig', () => ({ * @type {string | undefined} */ redisNatmap: process.env.REDIS_NATMAP, + + /** + * Interval for health checks in milliseconds. + * Defines how frequently the application will perform health checks on registered servers. + * - Default: 30000 ms (30 seconds). + * - Can be overridden by setting the environment variable `HEALTH_CHECK_INTERVAL`. + */ + healthCheckInterval: process.env.HEALTH_CHECK_INTERVAL || 30000, })) diff --git a/packages/loadbalancer/src/config/logger.config.ts b/packages/loadbalancer/src/config/logger.config.ts index 6da20ab..49b8efc 100644 --- a/packages/loadbalancer/src/config/logger.config.ts +++ b/packages/loadbalancer/src/config/logger.config.ts @@ -1,10 +1,11 @@ import { LogLevel, Logger } from '@nestjs/common' +import { warn } from 'console' export function getLogLevels(): LogLevel[] { const logger = new Logger('getLogLevels') const logLevelConfig = parseInt(process.env.LOG_LEVEL, 10) || 1 - const logLevels: LogLevel[] = ['log'] // Default to 'log' + const logLevels: LogLevel[] = ['log', 'warn'] // Default to 'log' // Adjust log levels based on the configuration switch (logLevelConfig) { diff --git a/packages/loadbalancer/src/dto/rooms.dto.ts b/packages/loadbalancer/src/dto/rooms.dto.ts index d458d0d..c03afb9 100644 --- a/packages/loadbalancer/src/dto/rooms.dto.ts +++ b/packages/loadbalancer/src/dto/rooms.dto.ts @@ -84,9 +84,7 @@ export interface ServerData { * Interface for available server */ export interface AvailableServer extends ServerData { - load: number capacity: number - consumers: number } /** diff --git a/packages/loadbalancer/src/lib/HttpRequestService.ts b/packages/loadbalancer/src/lib/HttpRequestService.ts index 2bc5a1a..bb99178 100644 --- a/packages/loadbalancer/src/lib/HttpRequestService.ts +++ b/packages/loadbalancer/src/lib/HttpRequestService.ts @@ -25,7 +25,7 @@ export class HttpRequestService { try { const response = await firstValueFrom(this.httpService.post(uri, data)) this.logger.log(`[post] Request sent to ${uri}: ${response.status}`) - return response.data + return response } catch (error) { this.logger.error(`[post] Failed to send POST request to ${uri}: ${error.message}`) } @@ -45,7 +45,7 @@ export class HttpRequestService { try { const response = await firstValueFrom(this.httpService.get(uri)) this.logger.log(`[get] Request sent to ${uri}: ${response.status}`) - return response.data + return response } catch (error) { this.logger.error(`[get] Failed to send GET request to ${uri}: ${error.message}`) throw error diff --git a/packages/loadbalancer/src/lib/ServerHealthChecker.ts b/packages/loadbalancer/src/lib/ServerHealthChecker.ts new file mode 100644 index 0000000..c26c48f --- /dev/null +++ b/packages/loadbalancer/src/lib/ServerHealthChecker.ts @@ -0,0 +1,94 @@ +import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common' +import Redis from 'ioredis' +import { InjectRedis } from '@nestjs-modules/ioredis' +import { HttpRequestService } from './HttpRequestService' +import { ConfigService } from '@nestjs/config' +import { Server } from 'https' + +@Injectable() +export class ServerHealthChecker implements OnModuleInit, OnModuleDestroy { + private readonly logger = new Logger(ServerHealthChecker.name) + private readonly interval: number // Interval in milliseconds + private healthCheckInterval: NodeJS.Timeout + + constructor( + @InjectRedis() private readonly redisClient: Redis, + private readonly httpRequestService: HttpRequestService, + private readonly configService: ConfigService, + ) { + // Load interval from environment variables or default to 30 seconds + + this.interval = Number(configService.get('appConfig.healthCheckInterval')) || 30000 + } + + /** + * Lifecycle hook that starts the health check process when the module is initialized. + */ + onModuleInit(): void { + this.logger.log(`Starting health checks every ${this.interval} ms.`) + this.startHealthChecks() + } + + /** + * Lifecycle hook that clears the interval when the module is destroyed. + */ + onModuleDestroy(): void { + this.stopHealthChecks() + } + + /** + * Starts periodic health checks. + */ + private startHealthChecks(): void { + this.healthCheckInterval = setInterval(async () => { + try { + await this.checkAllServers() + } catch (error) { + this.logger.error(`Error during health checks: ${error.message}`) + } + }, this.interval) + } + + /** + * Stops the health check process. + */ + private stopHealthChecks(): void { + if (this.healthCheckInterval) { + clearInterval(this.healthCheckInterval) + this.logger.log('Health checks stopped.') + } + } + + /** + * Checks the health of all registered servers. + */ + private async checkAllServers(): Promise { + this.logger.log('Checking health of all servers.') + const keys = await this.redisClient.keys('server:*') + for (const key of keys) { + const serverData = await this.redisClient.hgetall(key) + const { url } = serverData + + const serverId = key.split(':')[1] + this.logger.debug(`ServerId: ${serverId}, URL: ${url}`) + + if (!url) { + this.logger.warn(`Server ${key.split(':')[1]} has no URL registered.`) + continue + } + + try { + const response = await this.httpRequestService.get(`${url}/rooms/12345`) + if (response.status === 200) { + await this.redisClient.hset(key, 'health', 'true') + this.logger.log(`Server ${serverId} is healthy.`) + } else { + throw new Error(`Unexpected response status: ${response.status}`) + } + } catch (error) { + await this.redisClient.hset(key, 'health', 'false') + this.logger.warn(`Server ${serverId} is unhealthy: ${error.message}`) + } + } + } +} diff --git a/packages/loadbalancer/src/loadbalancer.controller.ts b/packages/loadbalancer/src/loadbalancer.controller.ts index 1046d37..1e8f482 100644 --- a/packages/loadbalancer/src/loadbalancer.controller.ts +++ b/packages/loadbalancer/src/loadbalancer.controller.ts @@ -17,7 +17,7 @@ export class LoadbalancerController { * @param {CreateRoomDto} createRoomDto - Parameters for room creation. * @returns {object} - Room details and WebSocket URL. */ - @Post(':roomId?') + @Post('rooms/:roomId?') @ApiOperation({ summary: 'Create or retrieve a room' }) @ApiParam({ name: 'roomId', @@ -53,6 +53,7 @@ export class LoadbalancerController { async createRoom(@Param('roomId') roomId: string, @Body() createRoomDto: CreateRoomDto) { const { eventNotificationUri, maxPeerCount } = createRoomDto try { + this.logger.debug(`Init Controller CreateRomm`) return await this.loadbalancerService.createRoom(roomId, eventNotificationUri, maxPeerCount) } catch (error) { this.logger.error(`${error.message}`) diff --git a/packages/loadbalancer/src/loadbalancer.module.ts b/packages/loadbalancer/src/loadbalancer.module.ts index 3e0ff11..69ed04c 100644 --- a/packages/loadbalancer/src/loadbalancer.module.ts +++ b/packages/loadbalancer/src/loadbalancer.module.ts @@ -5,6 +5,7 @@ import { ConfigModule } from '@nestjs/config' import appConfig from './config/app.config' import { HandledRedisModule } from './modules/redis.module' import { HttpRequestService } from './lib/HttpRequestService' +import { ServerHealthChecker } from './lib/ServerHealthChecker' @Module({ imports: [ @@ -15,6 +16,6 @@ import { HttpRequestService } from './lib/HttpRequestService' HandledRedisModule, ], controllers: [LoadbalancerController], - providers: [LoadbalancerService, HttpRequestService], + providers: [LoadbalancerService, ServerHealthChecker, HttpRequestService], }) export class LoadbalancerModule {} diff --git a/packages/loadbalancer/src/loadbalancer.service.ts b/packages/loadbalancer/src/loadbalancer.service.ts index a8d650c..c6cbf8d 100644 --- a/packages/loadbalancer/src/loadbalancer.service.ts +++ b/packages/loadbalancer/src/loadbalancer.service.ts @@ -35,6 +35,7 @@ export class LoadbalancerService { // Retrieve the best server based on load const bestServer = await this.getBestServer() + if (!bestServer) { this.logger.error('[createRoom] No servers available to handle the request.') throw new Error('[createRoom] No servers available') @@ -48,17 +49,26 @@ export class LoadbalancerService { maxPeerCount, } + // Construct the URL dynamically based on the presence of roomId + const url = roomId ? `${bestServer.url}/rooms/${roomId}` : `${bestServer.url}/rooms` + // Send a POST request to the selected server to create the room - const response = await this.httpRequestService.post(`${bestServer.url}/rooms/${roomId}`, roomCreationData) + const response = await this.httpRequestService.post(url, roomCreationData) + + // Validate the response + if (response.status !== 200) { + this.logger.error(`[createRoom] Failed to create room. Response: ${response} `) + throw new Error(`[createRoom] Failed to create room on server ${bestServer.url}`) + } this.logger.log( - `[createRoom] Room created successfully on server ${bestServer.url}. Response: ${JSON.stringify(response)}`, + `[createRoom] Room created successfully on server ${bestServer.url}. Response: ${response.status}`, ) // Update the server load - await this.updateLoadServer(bestServer.serverId, roomId, maxPeerCount || 2) + await this.updateLoadServer(bestServer.serverId, response.data.roomId, maxPeerCount || 2) // Return the response containing room details - return response + return response.data } catch (error) { this.logger.error(`[createRoom] Error creating or retrieving room: ${error.message}`) throw new HttpException({ error: error.message }, HttpStatus.INTERNAL_SERVER_ERROR) @@ -114,13 +124,12 @@ export class LoadbalancerService { } /** - * Retrieves the list of available servers sorted by load. - * Load is calculated as the used capacity divided by the maximum capacity. + * Retrieves the server with the most capacity among all available servers. * - * @returns {Promise} - List of servers with their load and capacity. - * @throws {HttpException} - Throws if Redis fails to retrieve server information. + * @returns {Promise} - The server with the most capacity. + * @throws {HttpException} - Throws if no servers are available or Redis fails. */ - public async getAvailableServers(): Promise { + public async getAvailableServers(): Promise { try { this.logger.log('[getAvailableServers] Fetching available servers from Redis.') const keys = await this.redisClient.keys('server:*') @@ -129,11 +138,8 @@ export class LoadbalancerService { for (const key of keys) { const serverData = await this.redisClient.hgetall(key) - // Calculate total consumers across all rooms for this server - const totalConsumers = await this.calculateConsumersForServer(serverData.serverId) - + // Parse and prepare the server data const capacity = Number(serverData.capacity) - const load = totalConsumers / capacity const workers = Number(serverData.workers) servers.push({ @@ -141,15 +147,23 @@ export class LoadbalancerService { workers, url: serverData.url, capacity, - load, - consumers: totalConsumers, }) } - // Sort servers by load (lower load first) - return servers.sort((a, b) => a.load - b.load) + if (servers.length === 0) { + this.logger.warn('[getAvailableServers] No servers available.') + throw new Error('No servers available.') + } + + // Find the server with the most capacity + const serverWithMostCapacity = servers.reduce((max, server) => (server.capacity > max.capacity ? server : max)) + + this.logger.log( + `[getAvailableServers] Server with the most capacity: ${JSON.stringify(serverWithMostCapacity, null, 2)}`, + ) + return serverWithMostCapacity } catch (error) { - this.logger.error(`[getAvailableServers] Failed to retrieve available servers: ${error.message}`) + this.logger.error(`[getAvailableServers] Failed to retrieve servers: ${error.message}`) throw new HttpException({ error: error.message }, HttpStatus.INTERNAL_SERVER_ERROR) } } @@ -163,12 +177,12 @@ export class LoadbalancerService { public async getBestServer(): Promise { try { const servers = await this.getAvailableServers() - if (servers.length === 0) { + if (!servers) { this.logger.warn('[getBestServer] No servers available.') throw new Error('[getBestServer] No servers available.') } - const bestServer = servers[0] // Select the server with the lowest load + const bestServer = servers this.logger.log(`[getBestServer] Best server selected: ${JSON.stringify(bestServer)}`) return bestServer } catch (error) { @@ -177,28 +191,9 @@ export class LoadbalancerService { } } - /** - * Calculates the total number of consumers for all rooms on a given server. - * @param {string} serverId - The ID of the server. - * @returns {Promise} - Total number of consumers. - */ - private async calculateConsumersForServer(serverId: string): Promise { - const roomKeys = await this.redisClient.keys(`room:${serverId}:*`) - let totalConsumers = 0 - - for (const roomKey of roomKeys) { - const peerCount = await this.redisClient.hget(roomKey, 'peers') - if (peerCount) { - const peers = Number(peerCount) - totalConsumers += peers * (peers - 1) * 2 - } - } - - return totalConsumers - } - /** * Registers a WebRTC server in Redis. + * If the server is already registered, it removes the old entry before adding the new one. * Calculates the server's capacity based on the number of workers and stores it in Redis. * * @param {ServerData} serverData - The server data to register. @@ -213,10 +208,18 @@ export class LoadbalancerService { throw new HttpException('Invalid server data', HttpStatus.BAD_REQUEST) } + // Check if the server is already registered + const key = `server:${serverId}` + const exists = await this.redisClient.exists(key) + + if (exists) { + this.logger.warn(`Server ${serverId} is already registered. Removing the old entry.`) + await this.redisClient.del(key) + } + // Calculate maximum capacity based on the number of workers const capacity = workers * 500 // 500 consumers per worker - const key = `server:${serverId}` await this.redisClient.hset( key, 'url', @@ -224,10 +227,14 @@ export class LoadbalancerService { 'capacity', capacity.toString(), 'rooms', - '0', // Initial number of active rooms is 0 + '0', + 'workers', + workers.toString(), + 'health', + 'true', ) - this.logger.log(`Server registered: ${JSON.stringify({ serverId, url, workers, capacity })}`) + this.logger.log(`Server registered: ${JSON.stringify({ serverId, url, workers, capacity, health: true })}`) } /** From 1d6fb54a423ccb635680356f039ebccabbe5e459 Mon Sep 17 00:00:00 2001 From: Gabriel Mata Date: Wed, 29 Jan 2025 17:29:03 -0500 Subject: [PATCH 04/15] refactor: attrib controllers and service of load balancer --- .../dto/{rooms.dto.ts => loadbalancer.dto.ts} | 22 ++- .../src/lib/ServerHealthChecker.ts | 10 +- .../src/loadbalancer.controller.ts | 27 ++-- .../loadbalancer/src/loadbalancer.service.ts | 148 ++++++++++-------- 4 files changed, 124 insertions(+), 83 deletions(-) rename packages/loadbalancer/src/dto/{rooms.dto.ts => loadbalancer.dto.ts} (79%) diff --git a/packages/loadbalancer/src/dto/rooms.dto.ts b/packages/loadbalancer/src/dto/loadbalancer.dto.ts similarity index 79% rename from packages/loadbalancer/src/dto/rooms.dto.ts rename to packages/loadbalancer/src/dto/loadbalancer.dto.ts index c03afb9..5159022 100644 --- a/packages/loadbalancer/src/dto/rooms.dto.ts +++ b/packages/loadbalancer/src/dto/loadbalancer.dto.ts @@ -74,9 +74,27 @@ export class RoomClosedDto { /** * Interface for server data */ -export interface ServerData { +export class ServerData { + @ApiProperty({ + description: 'Unique identifier for the server', + example: 'server-12345', + }) + @IsString({ message: 'serverId must be a string' }) serverId: string - url: string + + @ApiProperty({ + description: 'Service URL endpoint where create room', + example: 'http://webrtc.example/', + }) + @IsString({ message: 'serviceUrl must be a string' }) + serviceUrl: string + + @ApiProperty({ + description: 'Quantity of Workers of the server', + example: 4, + }) + @IsInt({ message: 'capacity must be an integer' }) + @Min(1, { message: 'capacity must be at least 1' }) workers: number } diff --git a/packages/loadbalancer/src/lib/ServerHealthChecker.ts b/packages/loadbalancer/src/lib/ServerHealthChecker.ts index c26c48f..c4e8046 100644 --- a/packages/loadbalancer/src/lib/ServerHealthChecker.ts +++ b/packages/loadbalancer/src/lib/ServerHealthChecker.ts @@ -18,7 +18,7 @@ export class ServerHealthChecker implements OnModuleInit, OnModuleDestroy { ) { // Load interval from environment variables or default to 30 seconds - this.interval = Number(configService.get('appConfig.healthCheckInterval')) || 30000 + this.interval = Number(this.configService.get('appConfig.healthCheckInterval')) } /** @@ -67,18 +67,18 @@ export class ServerHealthChecker implements OnModuleInit, OnModuleDestroy { const keys = await this.redisClient.keys('server:*') for (const key of keys) { const serverData = await this.redisClient.hgetall(key) - const { url } = serverData + const { serviceUrl } = serverData const serverId = key.split(':')[1] - this.logger.debug(`ServerId: ${serverId}, URL: ${url}`) + this.logger.debug(`ServerId: ${serverId}, URL: ${serviceUrl}`) - if (!url) { + if (!serviceUrl) { this.logger.warn(`Server ${key.split(':')[1]} has no URL registered.`) continue } try { - const response = await this.httpRequestService.get(`${url}/rooms/12345`) + const response = await this.httpRequestService.get(`${serviceUrl}/rooms/12345`) if (response.status === 200) { await this.redisClient.hset(key, 'health', 'true') this.logger.log(`Server ${serverId} is healthy.`) diff --git a/packages/loadbalancer/src/loadbalancer.controller.ts b/packages/loadbalancer/src/loadbalancer.controller.ts index 1e8f482..eca9e14 100644 --- a/packages/loadbalancer/src/loadbalancer.controller.ts +++ b/packages/loadbalancer/src/loadbalancer.controller.ts @@ -1,7 +1,7 @@ import { Body, Controller, Post, HttpException, HttpStatus, Logger, Param } from '@nestjs/common' import { LoadbalancerService } from './loadbalancer.service' import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger' -import { CreateRoomDto, ServerData } from './dto/rooms.dto' +import { CreateRoomDto, ServerData } from './dto/loadbalancer.dto' @ApiTags('Load Balancer') @Controller() @@ -53,11 +53,10 @@ export class LoadbalancerController { async createRoom(@Param('roomId') roomId: string, @Body() createRoomDto: CreateRoomDto) { const { eventNotificationUri, maxPeerCount } = createRoomDto try { - this.logger.debug(`Init Controller CreateRomm`) return await this.loadbalancerService.createRoom(roomId, eventNotificationUri, maxPeerCount) } catch (error) { - this.logger.error(`${error.message}`) - throw new HttpException(`${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR) + this.logger.error(`Error createRoom: ${error.message}`) + throw new HttpException(`Error createRoom: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR) } } @@ -92,12 +91,13 @@ export class LoadbalancerController { description: 'Invalid server data.', }) async registerServer(@Body() serverData: ServerData): Promise<{ message: string }> { - if (!serverData.serverId || !serverData.url || !serverData.workers) { - throw new HttpException('Invalid server data', HttpStatus.BAD_REQUEST) + try { + await this.loadbalancerService.registerServer(serverData) + return { message: 'Server registered successfully' } + } catch (error) { + this.logger.error(`Error registerServer: ${error.message}`) + throw new HttpException(`Error registerServer: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR) } - - await this.loadbalancerService.registerServer(serverData) - return { message: 'Server registered successfully' } } /** @@ -132,7 +132,12 @@ export class LoadbalancerController { description: 'Server or room not found.', }) async notifyRoomClosed(@Body() roomData: { serverId: string; roomId: string }): Promise<{ message: string }> { - await this.loadbalancerService.roomClosed(roomData) - return { message: 'Room closed notification processed successfully' } + try { + await this.loadbalancerService.roomClosed(roomData) + return { message: 'Room closed notification processed successfully' } + } catch (error) { + this.logger.error(`Error notifyRoomClosed: ${error.message}`) + throw new HttpException(`Error notifyRoomClosed: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR) + } } } diff --git a/packages/loadbalancer/src/loadbalancer.service.ts b/packages/loadbalancer/src/loadbalancer.service.ts index c6cbf8d..f38bc85 100644 --- a/packages/loadbalancer/src/loadbalancer.service.ts +++ b/packages/loadbalancer/src/loadbalancer.service.ts @@ -2,7 +2,7 @@ import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common' import { HttpRequestService } from './lib/HttpRequestService' import Redis from 'ioredis' import { InjectRedis } from '@nestjs-modules/ioredis' -import { AvailableServer, RoomData, RoomResponse, ServerData } from './dto/rooms.dto' +import { AvailableServer, RoomData, RoomResponse, ServerData } from './dto/loadbalancer.dto' @Injectable() export class LoadbalancerService { @@ -50,19 +50,15 @@ export class LoadbalancerService { } // Construct the URL dynamically based on the presence of roomId - const url = roomId ? `${bestServer.url}/rooms/${roomId}` : `${bestServer.url}/rooms` + const url = roomId ? `${bestServer.serviceUrl}/rooms/${roomId}` : `${bestServer.serviceUrl}/rooms` // Send a POST request to the selected server to create the room const response = await this.httpRequestService.post(url, roomCreationData) - // Validate the response - if (response.status !== 200) { - this.logger.error(`[createRoom] Failed to create room. Response: ${response} `) - throw new Error(`[createRoom] Failed to create room on server ${bestServer.url}`) - } + this.logger.debug(`Response.status: ${response.status}`) this.logger.log( - `[createRoom] Room created successfully on server ${bestServer.url}. Response: ${response.status}`, + `[createRoom] Room created successfully on server ${bestServer.serviceUrl}. Response: ${response.status}`, ) // Update the server load await this.updateLoadServer(bestServer.serverId, response.data.roomId, maxPeerCount || 2) @@ -124,20 +120,26 @@ export class LoadbalancerService { } /** - * Retrieves the server with the most capacity among all available servers. + * Retrieves the server with the most capacity among all available servers that are healthy. * - * @returns {Promise} - The server with the most capacity. - * @throws {HttpException} - Throws if no servers are available or Redis fails. + * @returns {Promise} - The healthiest server with the most capacity. + * @throws {HttpException} - Throws if no healthy servers are available or Redis fails. */ public async getAvailableServers(): Promise { try { - this.logger.log('[getAvailableServers] Fetching available servers from Redis.') + this.logger.log('[getAvailableServers] Fetching available healthy servers from Redis.') const keys = await this.redisClient.keys('server:*') const servers: AvailableServer[] = [] for (const key of keys) { const serverData = await this.redisClient.hgetall(key) + // Check health status + if (serverData.health !== 'true') { + this.logger.warn(`[getAvailableServers] Server ${serverData.serverId} is unhealthy, skipping.`) + continue + } + // Parse and prepare the server data const capacity = Number(serverData.capacity) const workers = Number(serverData.workers) @@ -145,25 +147,29 @@ export class LoadbalancerService { servers.push({ serverId: key.split(':')[1], workers, - url: serverData.url, + serviceUrl: serverData.serviceUrl, capacity, }) } if (servers.length === 0) { - this.logger.warn('[getAvailableServers] No servers available.') - throw new Error('No servers available.') + this.logger.warn('[getAvailableServers] No healthy servers available.') + throw new Error('No healthy servers available.') } - // Find the server with the most capacity + // Find the healthy server with the most capacity const serverWithMostCapacity = servers.reduce((max, server) => (server.capacity > max.capacity ? server : max)) this.logger.log( - `[getAvailableServers] Server with the most capacity: ${JSON.stringify(serverWithMostCapacity, null, 2)}`, + `[getAvailableServers] Selected healthiest server with most capacity: ${JSON.stringify( + serverWithMostCapacity, + null, + 2, + )}`, ) return serverWithMostCapacity } catch (error) { - this.logger.error(`[getAvailableServers] Failed to retrieve servers: ${error.message}`) + this.logger.error(`[getAvailableServers] Failed to retrieve healthy servers: ${error.message}`) throw new HttpException({ error: error.message }, HttpStatus.INTERNAL_SERVER_ERROR) } } @@ -200,13 +206,7 @@ export class LoadbalancerService { * @throws {HttpException} - If the server data is invalid. */ public async registerServer(serverData: ServerData): Promise { - const { serverId, url, workers } = serverData - - // Validate input - if (!serverId || !url || !workers || workers <= 0) { - this.logger.error('Invalid server data provided for registration.') - throw new HttpException('Invalid server data', HttpStatus.BAD_REQUEST) - } + const { serverId, serviceUrl, workers } = serverData // Check if the server is already registered const key = `server:${serverId}` @@ -222,8 +222,8 @@ export class LoadbalancerService { await this.redisClient.hset( key, - 'url', - url, + 'serviceUrl', + serviceUrl, 'capacity', capacity.toString(), 'rooms', @@ -234,7 +234,7 @@ export class LoadbalancerService { 'true', ) - this.logger.log(`Server registered: ${JSON.stringify({ serverId, url, workers, capacity, health: true })}`) + this.logger.log(`Server registered: ${JSON.stringify({ serverId, serviceUrl, workers, capacity, health: true })}`) } /** @@ -242,49 +242,67 @@ export class LoadbalancerService { * @param {RoomData} roomData - The data of the room that was closed. */ public async roomClosed(roomData: RoomData): Promise { - const serverKey = `server:${roomData.serverId}` - const roomKey = `room:${roomData.serverId}:${roomData.roomId}` - - // Check if the server exists - const serverExists = await this.redisClient.exists(serverKey) - if (!serverExists) { - this.logger.warn(`[roomClosed] Server ${roomData.serverId} not found in Redis.`) - return - } + try { + const serverKey = `server:${roomData.serverId}` + const roomKey = `room:${roomData.serverId}:${roomData.roomId}` + + // Check if the server exists + const serverExists = await this.redisClient.exists(serverKey) + if (!serverExists) { + this.logger.error(`[roomClosed] Server ${roomData.serverId} not found in Redis.`) + throw new HttpException(`Server ${roomData.serverId} not found.`, HttpStatus.NOT_FOUND) + } - // Check if the room exists - const roomExists = await this.redisClient.exists(roomKey) - if (!roomExists) { - this.logger.warn(`[roomClosed] Room ${roomData.roomId} not found on server ${roomData.serverId}.`) - return - } + // Check if the room exists + const roomExists = await this.redisClient.exists(roomKey) + if (!roomExists) { + this.logger.error(`[roomClosed] Room ${roomData.roomId} not found on server ${roomData.serverId}.`) + throw new HttpException( + `Room ${roomData.roomId} not found on server ${roomData.serverId}.`, + HttpStatus.NOT_FOUND, + ) + } - // Retrieve the number of peers in the room - const peerCount = await this.redisClient.hget(roomKey, 'peers') - if (!peerCount) { - this.logger.warn(`[roomClosed] Peer count not found for room ${roomData.roomId}.`) - return - } + // Retrieve the number of peers in the room + const peerCount = await this.redisClient.hget(roomKey, 'peers') + if (!peerCount) { + this.logger.warn(`[roomClosed] Peer count not found for room ${roomData.roomId}. Defaulting to 0.`) + throw new HttpException(`Peer count not found for room ${roomData.roomId}.`, HttpStatus.BAD_REQUEST) + } - // Calculate the total consumers for the room - const totalConsumers = Number(peerCount) * (Number(peerCount) - 1) * 2 + // Calculate the total consumers for the room + const peerCountNum = Number(peerCount) + if (isNaN(peerCountNum) || peerCountNum <= 0) { + this.logger.error(`[roomClosed] Invalid peer count: ${peerCount} for room ${roomData.roomId}.`) + throw new HttpException(`Invalid peer count for room ${roomData.roomId}.`, HttpStatus.BAD_REQUEST) + } - // Increment the server capacity - await this.redisClient.hincrby(serverKey, 'capacity', totalConsumers) + const totalConsumers = peerCountNum * (peerCountNum - 1) * 2 + + // Restore the server's capacity + await this.redisClient.hincrby(serverKey, 'capacity', totalConsumers) + + // Update the room count on the server + const currentRooms = await this.redisClient.hget(serverKey, 'rooms') + if (!currentRooms) { + this.logger.warn(`[roomClosed] Room count not found for server ${roomData.serverId}. Defaulting to 0.`) + } else if (Number(currentRooms) > 0) { + await this.redisClient.hincrby(serverKey, 'rooms', -1) + this.logger.log( + `[roomClosed] Room ${roomData.roomId} closed on server ${roomData.serverId}. Consumers freed: ${totalConsumers}`, + ) + } else { + this.logger.warn(`[roomClosed] Room count for server ${roomData.serverId} is already at 0.`) + } - // Decrement the room count - const currentRooms = await this.redisClient.hget(serverKey, 'rooms') - if (Number(currentRooms) > 0) { - await this.redisClient.hincrby(serverKey, 'rooms', -1) - this.logger.log( - `[roomClosed] Room ${roomData.roomId} closed on server ${roomData.serverId}. Consumers freed: ${totalConsumers}`, + // Remove the room data from Redis + await this.redisClient.del(roomKey) + this.logger.log(`[roomClosed] Room ${roomData.roomId} removed from Redis.`) + } catch (error) { + this.logger.error( + `[roomClosed] Error closing room ${roomData.roomId} on server ${roomData.serverId}: ${error.message}`, ) - } else { - this.logger.warn(`[roomClosed] Room count for server ${roomData.serverId} is already 0.`) + throw new HttpException({ error: error.message }, HttpStatus.INTERNAL_SERVER_ERROR) } - - // Remove room data from Redis - await this.redisClient.del(roomKey) - this.logger.log(`[roomClosed] Room ${roomData.roomId} removed from Redis.`) } } From f97b1fe97e87aecd090edbf0a2b1c49ffc95e773 Mon Sep 17 00:00:00 2001 From: Gabriel Mata Date: Wed, 29 Jan 2025 17:45:43 -0500 Subject: [PATCH 05/15] docs: add README.md file --- packages/loadbalancer/README.md | 201 ++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 packages/loadbalancer/README.md diff --git a/packages/loadbalancer/README.md b/packages/loadbalancer/README.md new file mode 100644 index 0000000..e767ec9 --- /dev/null +++ b/packages/loadbalancer/README.md @@ -0,0 +1,201 @@ +# Load Balancer for WebRTC Mediasoup Servers + +## 🚀 Overview + +This project implements a **load balancer** for WebRTC **Mediasoup** servers. It enables **automatic server registration**, **health monitoring**, and **dynamic load balancing** to efficiently distribute WebRTC rooms among the available servers, ensuring a scalable and robust architecture. + +--- + +## ⚡ **Key Features** + +### **Server Registration** + +- **Dynamic server registration**: Servers register themselves dynamically when they start up. +- **Duplicate prevention**: If a server is already registered, the previous entry is removed and updated. +- **Capacity calculation**: Each server's **total capacity** is computed based on the number of workers (`workers × 500 consumers`). +- **Storage in Redis**: All registered servers are stored in Redis for fast retrieval. + +### **Health Monitoring & Auto-Removal** + +- **Scheduled health checks**: A background process continuously monitors all registered servers. +- **HTTP-based ping test**: Each server is periodically tested via an HTTP `GET` request to the `/health` endpoint. +- **Automatic deactivation**: If a server is unresponsive or returns a non-200 status code, it is marked as **unhealthy (`health: false`)**. +- **Reactivation**: If a previously unhealthy server becomes responsive, it is marked as **healthy (`health: true`)**. + +### **Room Allocation & Load Balancing** + +- **Load-aware room allocation**: When creating a room, the load balancer selects the server with the **highest available capacity**. +- **Capacity tracking**: Each time a room is created, the number of **expected consumers** is subtracted from the server’s capacity. +- **Room deallocation**: When a room is closed, the consumers are freed, and the server’s available capacity is restored. + +### **Fault Tolerance & Scalability** + +- **Multi-instance support**: The system can be deployed in a **distributed** environment with multiple load balancer instances. +- **Event-driven updates**: Server states and capacity updates are immediately reflected across all instances via Redis. +- **Automatic failover**: If a WebRTC server becomes unavailable, new rooms are assigned only to healthy servers. + +--- + +## 📌 **Endpoints** + +### ✅ **Register a WebRTC Server** + +Registers a new Mediasoup server, calculating its total capacity. + +**`POST /register`** + +#### 🔹 Request Body: + +```json +{ + "serverId": "server-001", + "serviceUrl": "http://example.com", + "workers": 4 +} +``` + +#### 🔹 Response: + +```json +{ "message": "Server registered successfully" } +``` + +--- + +### ✅ **Create or Retrieve a Room** + +Selects the most capable healthy server and assigns the room. + +**`POST rooms/:roomId?`** + +#### 🔹 Request Body: + +```json +{ + "eventNotificationUri": "http://example.com/notify", + "maxPeerCount": 3 +} +``` + +#### 🔹 Response: + +```json +{ + "protocol": "2060-mediasoup-v1", + "wsUrl": "wss://example.com:443", + "roomId": "room-67890" +} +``` + +--- + +### ✅ **Notify Room Closure** + +Updates the server's available capacity when a room is closed. + +**`POST /room-closed`** + +#### 🔹 Request Body: + +```json +{ + "serverId": "server-001", + "roomId": "room-67890" +} +``` + +#### 🔹 Response: + +```json +{ "message": "Room closed notification processed successfully" } +``` + +**Also you can use swagger documentation visit url:** `http://loadbalancer.domain/API` + +--- + +## ⚙️ **Configuration** + +The application uses **environment variables** for configuration. + +| Variable | Description | Default Value | +| ----------------------- | -------------------------------------------------- | ------------------------ | +| `APP_PORT` | The port number on which the application will run. | `3001` | +| `LOG_LEVEL` | Define log level of the application. 1 to | `1` | +| `REDIS_URL` | Redis connection URL | `redis://localhost:6379` | +| `HEALTH_CHECK_INTERVAL` | Health check webrtc-server frequency (ms). | `30000` | + +--- + +## 🏗️ **Architecture & Implementation** + +### 🔹 **1. Server Registration Flow** + +1. A Mediasoup server sends a `POST /register` request. +2. If the server is already registered, its entry is removed and re-added. +3. The server’s total capacity is calculated (`workers × 500` consumers). +4. The server is stored in Redis with `health: true`. + +### 🔹 **2. Health Check Process** + +1. Every **30 seconds** (or as defined in `HEALTH_CHECK_INTERVAL`), the system checks all registered servers. +2. A `GET /health` request is sent to each server. +3. If the server is **healthy**, its `health` status remains `true`. +4. If the server is **unhealthy**, its `health` status is updated to `false`. + +### 🔹 **3. Room Allocation Flow** + +1. A client requests to **create a room** (`POST rooms/:roomId?`). +2. The load balancer filters out **unhealthy servers**. +3. The server with the **highest available capacity** is selected. +4. The expected consumers for the room are **subtracted** from the server’s capacity. +5. The room is created, and the WebSocket URL is returned. + +### 🔹 **4. Room Closure Flow** + +1. When a room is **closed**, a `POST /room-closed` request is sent. +2. The number of consumers freed is **added back** to the server’s available capacity. +3. The room entry is **deleted** from Redis. + +--- + +## 🔧 **Deployment & Scaling** + +### 1️ **Running with Docker** + +```sh +docker-compose up --build +``` + +### 2️ **Deploying in Kubernetes** + +The load balancer can run **as a microservice** in Kubernetes. + +Example `deployment.yaml`: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: webrtc-loadbalancer +spec: + replicas: 3 + template: + spec: + containers: + - name: webrtc-loadbalancer + image: webrtc-loadbalancer:latest + env: + - name: REDIS_HOST + value: 'redis-service' + - name: HEALTH_CHECK_INTERVAL + value: '30000' +``` + +--- + +## 📜 **License** + +This project is licensed under the **Apache-2.0**. + +--- From 74667b110752aca496f2dd3aa8d92f77b832fec1 Mon Sep 17 00:00:00 2001 From: Gabriel Mata Date: Wed, 29 Jan 2025 18:06:10 -0500 Subject: [PATCH 06/15] fix: Url endpoit health test. --- packages/loadbalancer/src/lib/ServerHealthChecker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/loadbalancer/src/lib/ServerHealthChecker.ts b/packages/loadbalancer/src/lib/ServerHealthChecker.ts index c4e8046..c06964a 100644 --- a/packages/loadbalancer/src/lib/ServerHealthChecker.ts +++ b/packages/loadbalancer/src/lib/ServerHealthChecker.ts @@ -78,7 +78,7 @@ export class ServerHealthChecker implements OnModuleInit, OnModuleDestroy { } try { - const response = await this.httpRequestService.get(`${serviceUrl}/rooms/12345`) + const response = await this.httpRequestService.get(`${serviceUrl}/rooms/health`) if (response.status === 200) { await this.redisClient.hset(key, 'health', 'true') this.logger.log(`Server ${serverId} is healthy.`) From 4052101af7adfb5cf6e5fcb21a599c1563113097 Mon Sep 17 00:00:00 2001 From: Gabriel Mata Date: Thu, 30 Jan 2025 13:19:18 -0500 Subject: [PATCH 07/15] feat: add configurable integration webrtc-server with loadbalancer module. --- README.md | 24 +++++---- package.json | 2 +- .../loadbalancer/src/config/logger.config.ts | 1 - .../loadbalancer/src/dto/loadbalancer.dto.ts | 2 +- .../src/lib/HttpRequestService.ts | 12 +++-- .../src/lib/ServerHealthChecker.ts | 1 - .../src/loadbalancer.controller.spec.ts | 22 ++++---- packages/loadbalancer/test/app.e2e-spec.ts | 28 ++++------- packages/webrtc-server/docker-compose.yml | 27 +++++----- .../webrtc-server/src/config/app.config.ts | 17 +++++++ .../webrtc-server/src/config/config.server.ts | 1 + packages/webrtc-server/src/lib/Room.ts | 9 ++++ .../src/lib/notification.service.ts | 21 ++++++++ .../src/rooms/rooms.controller.ts | 24 +++++++++ .../src/rooms/rooms.service.spec.ts | 37 +++++++++++--- .../webrtc-server/src/rooms/rooms.service.ts | 50 +++++++++++++++++-- 16 files changed, 208 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index e1b583b..8fd38e3 100644 --- a/README.md +++ b/README.md @@ -46,17 +46,19 @@ To configure and build the `ICE Server` you can be use following enviroment vari Additional variables for configuring the `webrtc-server`: -| Variable | Description | Default Value | -| ------------------------ | --------------------------------------------------------- | ----------------------------------- | -| `PROTOO_LISTEN_PORT` | Port for the protoo WebSocket server and HTTP API server. | `4443` | -| `HTTPS_CERT_FULLCHAIN` | Path to the fullchain certificate file for HTTPS. | `/certs/fullchain.pem` | -| `HTTPS_CERT_PRIVKEY` | Path to the private key file for HTTPS. | `/certs/privkey.pem` | -| `MEDIASOUP_INGRESS_HOST` | Ingress host for the mediasoup client. | | -| `MEDIASOUP_MIN_PORT` | Minimum port for RTC connections in mediasoup. | `40000` | -| `MEDIASOUP_MAX_PORT` | Maximum port for RTC connections in mediasoup. | `49999` | -| `MEDIASOUP_LISTEN_IP` | The listening IP for audio/video in mediasoup. | `0.0.0.0` or `127.0.0.1` | -| `MEDIASOUP_ANNOUNCED_IP` | Public IP address for audio/video in mediasoup.. | | -| `MEDIASOUP_INGRESS_HOST` | Set Ingress host for /rooms response | | +| Variable | Description | Default Value | +| ------------------------ | -------------------------------------------------------------------------------------------------------- | ----------------------------------- | +| `PROTOO_LISTEN_PORT` | Port for the protoo WebSocket server and HTTP API server. | `4443` | +| `HTTPS_CERT_FULLCHAIN` | Path to the fullchain certificate file for HTTPS. | `/certs/fullchain.pem` | +| `HTTPS_CERT_PRIVKEY` | Path to the private key file for HTTPS. | `/certs/privkey.pem` | +| `MEDIASOUP_INGRESS_HOST` | Ingress host for the mediasoup client. | | +| `MEDIASOUP_MIN_PORT` | Minimum port for RTC connections in mediasoup. | `40000` | +| `MEDIASOUP_MAX_PORT` | Maximum port for RTC connections in mediasoup. | `49999` | +| `MEDIASOUP_LISTEN_IP` | The listening IP for audio/video in mediasoup. | `0.0.0.0` or `127.0.0.1` | +| `MEDIASOUP_ANNOUNCED_IP` | Public IP address for audio/video in mediasoup.. | | +| `MEDIASOUP_INGRESS_HOST` | Set Ingress host for /rooms response | | +| `LOADBALANCER_URL` | Specifies the URL of the load balancer responsible for distributing WebRTC rooms among available servers | | +| `SERVICE_URL` | Defines the base URL of the WebRTC server that registers itself with the load balancer | | ## Diagram of solution webrtc-server diff --git a/package.json b/package.json index 26e5a78..8d581fc 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "scripts": { "build": "nest build", "format": "prettier --write \"packages/**/*.ts\" \"libs/**/*.ts\"", - "check-types": "eslint \"{src,apps,libs,test}/**/*.ts\" --ignore-pattern demo/", + "check-types": "eslint \"{src,packages,libs,test}/**/*.ts\" --ignore-pattern demo/", "start": "nest start", "start:dev": "LOG_LEVEL=3 nest start --watch", "start:debug": "nest start --debug --watch", diff --git a/packages/loadbalancer/src/config/logger.config.ts b/packages/loadbalancer/src/config/logger.config.ts index 49b8efc..39d18e5 100644 --- a/packages/loadbalancer/src/config/logger.config.ts +++ b/packages/loadbalancer/src/config/logger.config.ts @@ -1,5 +1,4 @@ import { LogLevel, Logger } from '@nestjs/common' -import { warn } from 'console' export function getLogLevels(): LogLevel[] { const logger = new Logger('getLogLevels') diff --git a/packages/loadbalancer/src/dto/loadbalancer.dto.ts b/packages/loadbalancer/src/dto/loadbalancer.dto.ts index 5159022..eddda46 100644 --- a/packages/loadbalancer/src/dto/loadbalancer.dto.ts +++ b/packages/loadbalancer/src/dto/loadbalancer.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger' -import { IsString, IsUrl, IsInt, IsUUID, Min, IsOptional } from 'class-validator' +import { IsString, IsUrl, IsInt, Min, IsOptional } from 'class-validator' /** * DTO for creating a room diff --git a/packages/loadbalancer/src/lib/HttpRequestService.ts b/packages/loadbalancer/src/lib/HttpRequestService.ts index bb99178..44e7538 100644 --- a/packages/loadbalancer/src/lib/HttpRequestService.ts +++ b/packages/loadbalancer/src/lib/HttpRequestService.ts @@ -3,6 +3,7 @@ import { HttpService } from '@nestjs/axios' import { firstValueFrom } from 'rxjs' import { AxiosInstance } from 'axios' import axios from 'axios' +import https from 'https' @Injectable() export class HttpRequestService { @@ -10,7 +11,12 @@ export class HttpRequestService { private readonly httpService: HttpService constructor() { - const axiosInstance: AxiosInstance = axios.create() + const axiosInstance: AxiosInstance = axios.create({ + httpsAgent: new https.Agent({ + rejectUnauthorized: false, + }), + }) + this.httpService = new HttpService(axiosInstance) } @@ -27,7 +33,7 @@ export class HttpRequestService { this.logger.log(`[post] Request sent to ${uri}: ${response.status}`) return response } catch (error) { - this.logger.error(`[post] Failed to send POST request to ${uri}: ${error.message}`) + this.logger.error(`[post] Failed to send POST request to ${uri}: ${error}`) } } else { this.logger.warn(`[post] URI is not defined, cannot send POST request: ${JSON.stringify(data)}`) @@ -47,7 +53,7 @@ export class HttpRequestService { this.logger.log(`[get] Request sent to ${uri}: ${response.status}`) return response } catch (error) { - this.logger.error(`[get] Failed to send GET request to ${uri}: ${error.message}`) + this.logger.error(`[get] Failed to send GET request to ${uri}: ${error}`) throw error } } else { diff --git a/packages/loadbalancer/src/lib/ServerHealthChecker.ts b/packages/loadbalancer/src/lib/ServerHealthChecker.ts index c06964a..8600e3a 100644 --- a/packages/loadbalancer/src/lib/ServerHealthChecker.ts +++ b/packages/loadbalancer/src/lib/ServerHealthChecker.ts @@ -3,7 +3,6 @@ import Redis from 'ioredis' import { InjectRedis } from '@nestjs-modules/ioredis' import { HttpRequestService } from './HttpRequestService' import { ConfigService } from '@nestjs/config' -import { Server } from 'https' @Injectable() export class ServerHealthChecker implements OnModuleInit, OnModuleDestroy { diff --git a/packages/loadbalancer/src/loadbalancer.controller.spec.ts b/packages/loadbalancer/src/loadbalancer.controller.spec.ts index 2ca7aa0..cf1f6c9 100644 --- a/packages/loadbalancer/src/loadbalancer.controller.spec.ts +++ b/packages/loadbalancer/src/loadbalancer.controller.spec.ts @@ -1,22 +1,22 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { LoadbalancerController } from './loadbalancer.controller'; -import { LoadbalancerService } from './loadbalancer.service'; +import { Test, TestingModule } from '@nestjs/testing' +import { LoadbalancerController } from './loadbalancer.controller' +import { LoadbalancerService } from './loadbalancer.service' describe('LoadbalancerController', () => { - let loadbalancerController: LoadbalancerController; + let loadbalancerController: LoadbalancerController beforeEach(async () => { const app: TestingModule = await Test.createTestingModule({ controllers: [LoadbalancerController], providers: [LoadbalancerService], - }).compile(); + }).compile() - loadbalancerController = app.get(LoadbalancerController); - }); + loadbalancerController = app.get(LoadbalancerController) + }) describe('root', () => { it('should return "Hello World!"', () => { - expect(loadbalancerController.getHello()).toBe('Hello World!'); - }); - }); -}); + expect(loadbalancerController.getHello()).toBe('Hello World!') + }) + }) +}) diff --git a/packages/loadbalancer/test/app.e2e-spec.ts b/packages/loadbalancer/test/app.e2e-spec.ts index 50cda62..c0fc4d5 100644 --- a/packages/loadbalancer/test/app.e2e-spec.ts +++ b/packages/loadbalancer/test/app.e2e-spec.ts @@ -1,24 +1,16 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; -import { AppModule } from './../src/app.module'; +import { Test, TestingModule } from '@nestjs/testing' +import { INestApplication } from '@nestjs/common' +import { LoadbalancerModule } from './../src/loadbalancer.module' describe('AppController (e2e)', () => { - let app: INestApplication; + let app: INestApplication beforeEach(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); + imports: [LoadbalancerModule], + }).compile() - app = moduleFixture.createNestApplication(); - await app.init(); - }); - - it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect('Hello World!'); - }); -}); + app = moduleFixture.createNestApplication() + await app.init() + }) +}) diff --git a/packages/webrtc-server/docker-compose.yml b/packages/webrtc-server/docker-compose.yml index 705e59d..3b73d60 100644 --- a/packages/webrtc-server/docker-compose.yml +++ b/packages/webrtc-server/docker-compose.yml @@ -5,27 +5,32 @@ services: image: webrtc-server:test environment: - LOG_LEVEL=3 - - MEDIASOUP_ANNOUNCED_IP=192.168.100.84 - - MEDIASOUP_LISTEN_IP=192.168.100.84 + - MEDIASOUP_ANNOUNCED_IP=192.168.10.18 + - MEDIASOUP_LISTEN_IP=0.0.0.0 - PROTOO_LISTEN_PORT=4443 - MEDIASOUP_MIN_PORT=40000 - MEDIASOUP_MAX_PORT=50000 - HTTPS_CERT_FULLCHAIN=/config/certs/fullchain.pem - HTTPS_CERT_PRIVKEY=/config/certs/privkey.pem - MEDIASOUP_CLIENT_ENABLE_ICESERVER=yes - - MEDIASOUP_CLIENT_ICESERVER_HOST=192.168.100.84 + - MEDIASOUP_CLIENT_ICESERVER_HOST=192.168.10.18 - MEDIASOUP_CLIENT_ICESERVER_PROTO=udp - MEDIASOUP_CLIENT_ICESERVER_PORT=3478 - MEDIASOUP_CLIENT_ICESERVER_USER=test - MEDIASOUP_CLIENT_ICESERVER_PASS=test123 - - MEDIASOUP_CLIENT_PROTOOPORT=4443 + - MEDIASOUP_CLIENT_PROTOOPORT=443 - MEDIASOUP_INGRESS_HOST=webrtc.prueba.2060.io - REDIS_URL=redis://redis:6379 + - LOADBALANCER_URL=http://192.168.10.18:3000 + - SERVICE_URL=https://192.168.10.18 ports: - - '4443:4443' + - '443:4443' + networks: + - webrtc_network volumes: - ./certs/fullchain.pem:/config/certs/fullchain.pem - ./certs/privkey.pem:/config/certs/privkey.pem + coturn: image: coturn/coturn environment: @@ -35,11 +40,9 @@ services: ports: - '3478:3478' - '3478:3478/udp' + networks: + - webrtc_network - redis: - container_name: redis - image: redis:alpine - restart: always - ports: - - 6379:6379 - command: redis-server --maxmemory 64mb --maxmemory-policy allkeys-lru +networks: + webrtc_network: + driver: bridge diff --git a/packages/webrtc-server/src/config/app.config.ts b/packages/webrtc-server/src/config/app.config.ts index 4a52b0c..4a3d96f 100644 --- a/packages/webrtc-server/src/config/app.config.ts +++ b/packages/webrtc-server/src/config/app.config.ts @@ -40,4 +40,21 @@ export default registerAs('appConfig', () => ({ * @type {string | undefined} */ redisNatmap: process.env.REDIS_NATMAP, + + /** + * Load balancer base URL. + * Specifies the URL of the load balancer responsible for distributing WebRTC rooms among available servers. + * - Must be a valid URL. + * - Typically, this is the entry point for WebRTC server registration and room allocation. + * - Example: "http://loadbalancer.example.com" + */ + loadbalancerUrl: process.env.LOADBALANCER_URL, + + /** + * URL of the current service instance. + * - Defines the base URL of the WebRTC service or application that registers itself with the load balancer. + * - Used for health checks and room allocation. + * - Example: `http://webrtc-service.example.com` + */ + serviceUrl: process.env.SERVICE_URL, })) diff --git a/packages/webrtc-server/src/config/config.server.ts b/packages/webrtc-server/src/config/config.server.ts index 20b04d4..60471ca 100644 --- a/packages/webrtc-server/src/config/config.server.ts +++ b/packages/webrtc-server/src/config/config.server.ts @@ -10,6 +10,7 @@ export const config = { key: process.env.HTTPS_CERT_PRIVKEY || `${__dirname}/certs/privkey.pem`, }, ingressHost: process.env.MEDIASOUP_INGRESS_HOST, + protooPort: process.env.MEDIASOUP_CLIENT_PROTOOPORT, }, iceServers: [ { diff --git a/packages/webrtc-server/src/lib/Room.ts b/packages/webrtc-server/src/lib/Room.ts index 6f7f965..52e2e6c 100644 --- a/packages/webrtc-server/src/lib/Room.ts +++ b/packages/webrtc-server/src/lib/Room.ts @@ -195,6 +195,15 @@ export class Room extends EventEmitter { } await this.notificationService.sendNotification(eventNotificationUri, joinNotificationData) + + if (process.env.LOADBALANCER_URL) { + this.logger.debug(`**Send notification loadBalancer***`) + const loadbalancerUrl = `${process.env.LOADBALANCER_URL}/room-closed` + await this.notificationService.post(loadbalancerUrl, { + serverId: config.https.ingressHost, + roomId: this.roomId, + }) + } } // If the Peer was joined, notify all Peers. diff --git a/packages/webrtc-server/src/lib/notification.service.ts b/packages/webrtc-server/src/lib/notification.service.ts index 3566db2..e00764e 100644 --- a/packages/webrtc-server/src/lib/notification.service.ts +++ b/packages/webrtc-server/src/lib/notification.service.ts @@ -39,4 +39,25 @@ export class NotificationService { ) } } + + /** + * Sends a POST request to the specified URI with the given data. + * + * @param {string} uri - The URI to which the POST request will be sent. + * @param {Object} data - The payload to send in the POST request body. + */ + public async post(uri: string, data: object): Promise { + if (uri) { + try { + const response = await firstValueFrom(this.httpService.post(uri, data)) + this.logger.log(`[post] Request sent to ${uri}: ${response.status}`) + return response + } catch (error) { + this.logger.error(`[post] Failed to send POST request to ${uri}: ${error.message}`) + return error + } + } else { + this.logger.warn(`[post] URI is not defined, cannot send POST request: ${JSON.stringify(data)}`) + } + } } diff --git a/packages/webrtc-server/src/rooms/rooms.controller.ts b/packages/webrtc-server/src/rooms/rooms.controller.ts index 525e5da..8cec141 100644 --- a/packages/webrtc-server/src/rooms/rooms.controller.ts +++ b/packages/webrtc-server/src/rooms/rooms.controller.ts @@ -21,6 +21,30 @@ export class RoomsController { constructor(private readonly roomsService: RoomsService) {} + /** + * Health check endpoint for the WebRTC Server. + * - Used by the Load Balancer's `ServerHealthChecker` to verify server availability. + * - Returns `200 OK` if the WebRTC server is running correctly. + * - Can be expanded to check additional dependencies (e.g., WebRTC workers, database connections). + * + * @returns {Promise<{ status: string }>} - Returns `{ status: 'ok' }` if the server is healthy. + */ + @Get('health') + @ApiOperation({ summary: 'Health Check', description: 'Checks if the WebRTC server is running.' }) + @ApiResponse({ + status: 200, + description: 'The server is healthy.', + schema: { example: { status: 'ok' } }, + }) + @ApiResponse({ + status: 500, + description: 'Server error or dependency failure.', + }) + async checkHealth(): Promise<{ status: string }> { + this.logger.debug('Health check requested') + return this.roomsService.getHealthStatus() + } + /** * Endpoint to create or retrieve a room. * Delegates the logic to the RoomsService. diff --git a/packages/webrtc-server/src/rooms/rooms.service.spec.ts b/packages/webrtc-server/src/rooms/rooms.service.spec.ts index cc32bde..f727958 100644 --- a/packages/webrtc-server/src/rooms/rooms.service.spec.ts +++ b/packages/webrtc-server/src/rooms/rooms.service.spec.ts @@ -35,7 +35,21 @@ describe('RoomsService', () => { beforeEach(async () => { const module = await Test.createTestingModule({ - providers: [RoomsService, ConfigService], + providers: [ + { + provide: ConfigService, + useValue: { + get: jest.fn().mockImplementation((key: string) => { + const mockConfig = { + 'appConfig.loadbalancerUrl': 'http://mock-loadbalancer', + 'appConfig.serviceUrl': 'http://mock-service', + } + return mockConfig[key] || null + }), + }, + }, + RoomsService, + ], }).compile() service = module.get(RoomsService) @@ -147,6 +161,7 @@ describe('RoomsService - initServer Protoo', () => { let loggerErrorSpy: jest.SpyInstance let loggerLogSpy: jest.SpyInstance let mockHttpsServer: any + let mockConfigService: ConfigService beforeEach(async () => { const mockHttpsOptions = { @@ -154,7 +169,6 @@ describe('RoomsService - initServer Protoo', () => { cert: 'mockCert', } - // Mock the HTTPS server creation mockHttpsServer = { listen: jest.fn(), } @@ -168,13 +182,22 @@ describe('RoomsService - initServer Protoo', () => { Reflect.set(global, 'httpServer', mockHttpsServer) + // Mock de ConfigService + mockConfigService = { + get: jest.fn().mockImplementation((key: string) => { + const mockConfig = { + 'appConfig.loadbalancerUrl': 'http://mock-loadbalancer', + 'appConfig.serviceUrl': 'http://mock-service', + } + return mockConfig[key] || null + }), + } as unknown as ConfigService + const module = await Test.createTestingModule({ - providers: [RoomsService], + providers: [RoomsService, { provide: ConfigService, useValue: mockConfigService }], }).compile() service = module.get(RoomsService) - - // Spies for logger loggerErrorSpy = jest.spyOn(service['logger'], 'error').mockImplementation() loggerLogSpy = jest.spyOn(service['logger'], 'log').mockImplementation() }) @@ -187,7 +210,6 @@ describe('RoomsService - initServer Protoo', () => { it('should initialize the WebSocket server successfully and log the success message', () => { service['initServer']() - // Validate that the WebSocket server was initialized with the correct parameters expect(protoo.WebSocketServer).toHaveBeenCalledWith(mockHttpsServer, { maxReceivedFrameSize: 960000, maxReceivedMessageSize: 960000, @@ -195,10 +217,10 @@ describe('RoomsService - initServer Protoo', () => { fragmentationThreshold: 960000, }) - // Validate that the success log was called expect(loggerLogSpy).toHaveBeenCalledTimes(1) expect(loggerLogSpy).toHaveBeenCalledWith('[Protoo-server] WebSocket initialized.') }) + describe('onModuleInit', () => { it('should initialize the WebSocket server and handle connection requests', async () => { const mockOn = jest.fn() @@ -238,7 +260,6 @@ describe('RoomsService - initServer Protoo', () => { `protoo connection request [roomId:testRoom, peerId:testPeer, address:127.0.0.1, origin:testOrigin]`, ) }) - it('should log an error if handleConnection fails', async () => { const mockOn = jest.fn() const mockProtooServer = { diff --git a/packages/webrtc-server/src/rooms/rooms.service.ts b/packages/webrtc-server/src/rooms/rooms.service.ts index a7e022e..6f1d804 100644 --- a/packages/webrtc-server/src/rooms/rooms.service.ts +++ b/packages/webrtc-server/src/rooms/rooms.service.ts @@ -24,6 +24,7 @@ import * as protoo from 'protoo-server' import * as url from 'url' import { Server } from 'https' import { NotificationService } from '../lib/notification.service' +import { ConfigService } from '@nestjs/config' @Injectable() export class RoomsService implements OnModuleInit, OnModuleDestroy { @@ -37,7 +38,7 @@ export class RoomsService implements OnModuleInit, OnModuleDestroy { private protooServer: protoo.WebSocketServer private httpServer: Server - constructor() { + constructor(private readonly configService: ConfigService) { this.notificationService = new NotificationService() } @@ -47,6 +48,11 @@ export class RoomsService implements OnModuleInit, OnModuleDestroy { this.initServer() + // Register WebRTC Load Balancer + if (this.configService.get('appConfig.loadbalancerUrl')) { + this.registerLoadbalancer() + } + //Handle connections from clients this.protooServer.on('connectionrequest', (info, accept, reject) => { this.logger.log(`[Protoo-server] *** connectionrequest Listener ***`) @@ -98,7 +104,7 @@ export class RoomsService implements OnModuleInit, OnModuleDestroy { * Initializes the WebSocket server with specific configurations. * Sets up the server to listen on the specified port and defines a custom error. */ - private initServer(): void { + private async initServer(): Promise { try { this.httpServer = Reflect.get(global, 'httpServer') @@ -121,6 +127,33 @@ export class RoomsService implements OnModuleInit, OnModuleDestroy { } } + private async registerLoadbalancer(): Promise { + this.logger.log(`[registerLoadbalancer] WebRTC LoadBalancer mode enabled.`) + const loadbalancerUrl = `${this.configService.get('appConfig.loadbalancerUrl')}/register` + const serviceUrl = this.configService.get('appConfig.serviceUrl') + + if (loadbalancerUrl && serviceUrl) { + const dataRegister = { + serverId: config.https.ingressHost, + serviceUrl, + workers: config.mediasoup.numWorkers, + } + + try { + await this.notificationService.post(loadbalancerUrl, dataRegister) + this.logger.log(`[registerLoadbalancer] Successfully registered WebRTC server with Load Balancer.`) + } catch (error) { + this.logger.error( + `[registerLoadbalancer] Failed to register with WebRTC Load Balancer at ${loadbalancerUrl}: ${error.message}`, + ) + } + } else { + this.logger.warn( + `[registerLoadbalancer] LoadBalancer registration skipped: Missing loadbalancerUrl or serviceUrl.`, + ) + } + } + async handleConnection(roomId: string, peerId: string, accept: () => protoo.WebSocketTransport): Promise { let room = this.rooms.get(roomId) this.logger.debug(`[handleConnection] Initialize room: ${room}`) @@ -325,7 +358,7 @@ export class RoomsService implements OnModuleInit, OnModuleDestroy { const roomIdToUse = roomId ?? this.generateRandomRoomId() // Get WebSocket connection parameters - const port = config.https.listenPort + const port = config.https.protooPort const announcedIp = config.https.ingressHost const wsUrl = `wss://${announcedIp}:${port}` @@ -606,4 +639,15 @@ export class RoomsService implements OnModuleInit, OnModuleDestroy { private getRoomById(roomId: string): Room | undefined { return this.rooms.get(roomId) } + + /** + * Returns the health status of the WebRTC server. + * - If this endpoint is accessible, the server is considered "healthy". + * - Additional checks (e.g., WebRTC workers, database connections) can be added later. + * + * @returns {{ status: string }} - Always returns `{ status: 'ok' }` for now. + */ + public getHealthStatus(): { status: string } { + return { status: 'ok' } + } } From 5ad80593f9f851071501e6011b3bbc72dd960094 Mon Sep 17 00:00:00 2001 From: Gabriel Mata Date: Thu, 30 Jan 2025 13:23:42 -0500 Subject: [PATCH 08/15] fix: fix yarn format script into package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8d581fc..38f4cc8 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ }, "scripts": { "build": "nest build", - "format": "prettier --write \"packages/**/*.ts\" \"libs/**/*.ts\"", + "format": "prettier --write \"packages/**/*.ts\"", "check-types": "eslint \"{src,packages,libs,test}/**/*.ts\" --ignore-pattern demo/", "start": "nest start", "start:dev": "LOG_LEVEL=3 nest start --watch", From e57fe065f57d3e182020a8c78d90594316d8f9ee Mon Sep 17 00:00:00 2001 From: Gabriel Mata Date: Wed, 5 Feb 2025 15:37:28 -0500 Subject: [PATCH 09/15] refactor: change name monorepo folder ans remove reference .gitignore --- .gitignore | 2 +- .release-please-manifest.json | 4 ++-- {packages => apps}/loadbalancer/Dockerfile | 0 {packages => apps}/loadbalancer/README.md | 0 .../loadbalancer/docker-compose.yml | 0 {packages => apps}/loadbalancer/jest.config.ts | 0 {packages => apps}/loadbalancer/nest-cli.json | 0 {packages => apps}/loadbalancer/package.json | 0 .../loadbalancer/src/config/app.config.ts | 0 .../loadbalancer/src/config/logger.config.ts | 0 .../loadbalancer/src/dto/loadbalancer.dto.ts | 0 .../loadbalancer/src/lib/HttpRequestService.ts | 0 .../src/lib/ServerHealthChecker.ts | 0 .../src/loadbalancer.controller.spec.ts | 0 .../src/loadbalancer.controller.ts | 0 .../loadbalancer/src/loadbalancer.module.ts | 0 .../loadbalancer/src/loadbalancer.service.ts | 0 {packages => apps}/loadbalancer/src/main.ts | 0 .../loadbalancer/src/modules/redis.module.ts | 0 .../loadbalancer/test/app.e2e-spec.ts | 2 +- .../loadbalancer/test/jest-e2e.json | 0 .../loadbalancer/tsconfig.app.json | 2 +- .../loadbalancer/tsconfig.build.json | 0 {packages => apps}/loadbalancer/tsconfig.json | 0 {packages => apps}/webrtc-server/Dockerfile | 0 .../webrtc-server/docker-compose-lb.yml | 0 .../webrtc-server/docker-compose.yml | 0 .../webrtc-server/jest.config.ts | 0 {packages => apps}/webrtc-server/nest-cli.json | 0 {packages => apps}/webrtc-server/package.json | 4 ++-- .../webrtc-server/src/app.module.ts | 0 .../webrtc-server/src/config/app.config.ts | 0 .../webrtc-server/src/config/config.server.ts | 1 + .../webrtc-server/src/config/constants.ts | 0 .../webrtc-server/src/config/logger.config.ts | 0 .../webrtc-server/src/lib/Room.ts | 0 .../webrtc-server/src/lib/RoomFactory.ts | 0 .../src/lib/notification.service.ts | 0 .../webrtc-server/src/lib/room.interfaces.ts | 0 {packages => apps}/webrtc-server/src/main.ts | 0 .../webrtc-server/src/modules/redis.module.ts | 0 .../webrtc-server/src/rooms/dto/rooms.dto.ts | 0 .../src/rooms/rooms.controller.spec.ts | 0 .../src/rooms/rooms.controller.ts | 0 .../webrtc-server/src/rooms/rooms.module.ts | 0 .../src/rooms/rooms.service.spec.ts | 0 .../webrtc-server/src/rooms/rooms.service.ts | 0 .../webrtc-server/test/app.e2e-spec.ts | 0 .../webrtc-server/test/jest-e2e.json | 0 .../webrtc-server/tsconfig.app.json | 2 +- .../webrtc-server/tsconfig.build.json | 0 {packages => apps}/webrtc-server/tsconfig.json | 0 {packages => apps}/webrtc-server/yarn.lock | 0 jest.config.ts | 2 +- nest-cli.json | 18 +++++++++--------- package.json | 10 +++++----- release-please-config.json | 4 ++-- 57 files changed, 26 insertions(+), 25 deletions(-) rename {packages => apps}/loadbalancer/Dockerfile (100%) rename {packages => apps}/loadbalancer/README.md (100%) rename {packages => apps}/loadbalancer/docker-compose.yml (100%) rename {packages => apps}/loadbalancer/jest.config.ts (100%) rename {packages => apps}/loadbalancer/nest-cli.json (100%) rename {packages => apps}/loadbalancer/package.json (100%) rename {packages => apps}/loadbalancer/src/config/app.config.ts (100%) rename {packages => apps}/loadbalancer/src/config/logger.config.ts (100%) rename {packages => apps}/loadbalancer/src/dto/loadbalancer.dto.ts (100%) rename {packages => apps}/loadbalancer/src/lib/HttpRequestService.ts (100%) rename {packages => apps}/loadbalancer/src/lib/ServerHealthChecker.ts (100%) rename {packages => apps}/loadbalancer/src/loadbalancer.controller.spec.ts (100%) rename {packages => apps}/loadbalancer/src/loadbalancer.controller.ts (100%) rename {packages => apps}/loadbalancer/src/loadbalancer.module.ts (100%) rename {packages => apps}/loadbalancer/src/loadbalancer.service.ts (100%) rename {packages => apps}/loadbalancer/src/main.ts (100%) rename {packages => apps}/loadbalancer/src/modules/redis.module.ts (100%) rename {packages => apps}/loadbalancer/test/app.e2e-spec.ts (86%) rename {packages => apps}/loadbalancer/test/jest-e2e.json (100%) rename {packages => apps}/loadbalancer/tsconfig.app.json (78%) rename {packages => apps}/loadbalancer/tsconfig.build.json (100%) rename {packages => apps}/loadbalancer/tsconfig.json (100%) rename {packages => apps}/webrtc-server/Dockerfile (100%) rename {packages => apps}/webrtc-server/docker-compose-lb.yml (100%) rename {packages => apps}/webrtc-server/docker-compose.yml (100%) rename {packages => apps}/webrtc-server/jest.config.ts (100%) rename {packages => apps}/webrtc-server/nest-cli.json (100%) rename {packages => apps}/webrtc-server/package.json (93%) rename {packages => apps}/webrtc-server/src/app.module.ts (100%) rename {packages => apps}/webrtc-server/src/config/app.config.ts (100%) rename {packages => apps}/webrtc-server/src/config/config.server.ts (99%) rename {packages => apps}/webrtc-server/src/config/constants.ts (100%) rename {packages => apps}/webrtc-server/src/config/logger.config.ts (100%) rename {packages => apps}/webrtc-server/src/lib/Room.ts (100%) rename {packages => apps}/webrtc-server/src/lib/RoomFactory.ts (100%) rename {packages => apps}/webrtc-server/src/lib/notification.service.ts (100%) rename {packages => apps}/webrtc-server/src/lib/room.interfaces.ts (100%) rename {packages => apps}/webrtc-server/src/main.ts (100%) rename {packages => apps}/webrtc-server/src/modules/redis.module.ts (100%) rename {packages => apps}/webrtc-server/src/rooms/dto/rooms.dto.ts (100%) rename {packages => apps}/webrtc-server/src/rooms/rooms.controller.spec.ts (100%) rename {packages => apps}/webrtc-server/src/rooms/rooms.controller.ts (100%) rename {packages => apps}/webrtc-server/src/rooms/rooms.module.ts (100%) rename {packages => apps}/webrtc-server/src/rooms/rooms.service.spec.ts (100%) rename {packages => apps}/webrtc-server/src/rooms/rooms.service.ts (100%) rename {packages => apps}/webrtc-server/test/app.e2e-spec.ts (100%) rename {packages => apps}/webrtc-server/test/jest-e2e.json (100%) rename {packages => apps}/webrtc-server/tsconfig.app.json (78%) rename {packages => apps}/webrtc-server/tsconfig.build.json (100%) rename {packages => apps}/webrtc-server/tsconfig.json (100%) rename {packages => apps}/webrtc-server/yarn.lock (100%) diff --git a/.gitignore b/.gitignore index 3703f67..64b23b6 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,4 @@ lerna-debug.log* #files package-lock.json -packages/server/yarn.lock + diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 221042c..ecf4eb4 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,4 +1,4 @@ { - "packages/loadbalancer": "0.0.1", - "packages/webrtc-server": "0.0.1" + "apps/loadbalancer": "0.0.1", + "apps/webrtc-server": "0.0.1" } diff --git a/packages/loadbalancer/Dockerfile b/apps/loadbalancer/Dockerfile similarity index 100% rename from packages/loadbalancer/Dockerfile rename to apps/loadbalancer/Dockerfile diff --git a/packages/loadbalancer/README.md b/apps/loadbalancer/README.md similarity index 100% rename from packages/loadbalancer/README.md rename to apps/loadbalancer/README.md diff --git a/packages/loadbalancer/docker-compose.yml b/apps/loadbalancer/docker-compose.yml similarity index 100% rename from packages/loadbalancer/docker-compose.yml rename to apps/loadbalancer/docker-compose.yml diff --git a/packages/loadbalancer/jest.config.ts b/apps/loadbalancer/jest.config.ts similarity index 100% rename from packages/loadbalancer/jest.config.ts rename to apps/loadbalancer/jest.config.ts diff --git a/packages/loadbalancer/nest-cli.json b/apps/loadbalancer/nest-cli.json similarity index 100% rename from packages/loadbalancer/nest-cli.json rename to apps/loadbalancer/nest-cli.json diff --git a/packages/loadbalancer/package.json b/apps/loadbalancer/package.json similarity index 100% rename from packages/loadbalancer/package.json rename to apps/loadbalancer/package.json diff --git a/packages/loadbalancer/src/config/app.config.ts b/apps/loadbalancer/src/config/app.config.ts similarity index 100% rename from packages/loadbalancer/src/config/app.config.ts rename to apps/loadbalancer/src/config/app.config.ts diff --git a/packages/loadbalancer/src/config/logger.config.ts b/apps/loadbalancer/src/config/logger.config.ts similarity index 100% rename from packages/loadbalancer/src/config/logger.config.ts rename to apps/loadbalancer/src/config/logger.config.ts diff --git a/packages/loadbalancer/src/dto/loadbalancer.dto.ts b/apps/loadbalancer/src/dto/loadbalancer.dto.ts similarity index 100% rename from packages/loadbalancer/src/dto/loadbalancer.dto.ts rename to apps/loadbalancer/src/dto/loadbalancer.dto.ts diff --git a/packages/loadbalancer/src/lib/HttpRequestService.ts b/apps/loadbalancer/src/lib/HttpRequestService.ts similarity index 100% rename from packages/loadbalancer/src/lib/HttpRequestService.ts rename to apps/loadbalancer/src/lib/HttpRequestService.ts diff --git a/packages/loadbalancer/src/lib/ServerHealthChecker.ts b/apps/loadbalancer/src/lib/ServerHealthChecker.ts similarity index 100% rename from packages/loadbalancer/src/lib/ServerHealthChecker.ts rename to apps/loadbalancer/src/lib/ServerHealthChecker.ts diff --git a/packages/loadbalancer/src/loadbalancer.controller.spec.ts b/apps/loadbalancer/src/loadbalancer.controller.spec.ts similarity index 100% rename from packages/loadbalancer/src/loadbalancer.controller.spec.ts rename to apps/loadbalancer/src/loadbalancer.controller.spec.ts diff --git a/packages/loadbalancer/src/loadbalancer.controller.ts b/apps/loadbalancer/src/loadbalancer.controller.ts similarity index 100% rename from packages/loadbalancer/src/loadbalancer.controller.ts rename to apps/loadbalancer/src/loadbalancer.controller.ts diff --git a/packages/loadbalancer/src/loadbalancer.module.ts b/apps/loadbalancer/src/loadbalancer.module.ts similarity index 100% rename from packages/loadbalancer/src/loadbalancer.module.ts rename to apps/loadbalancer/src/loadbalancer.module.ts diff --git a/packages/loadbalancer/src/loadbalancer.service.ts b/apps/loadbalancer/src/loadbalancer.service.ts similarity index 100% rename from packages/loadbalancer/src/loadbalancer.service.ts rename to apps/loadbalancer/src/loadbalancer.service.ts diff --git a/packages/loadbalancer/src/main.ts b/apps/loadbalancer/src/main.ts similarity index 100% rename from packages/loadbalancer/src/main.ts rename to apps/loadbalancer/src/main.ts diff --git a/packages/loadbalancer/src/modules/redis.module.ts b/apps/loadbalancer/src/modules/redis.module.ts similarity index 100% rename from packages/loadbalancer/src/modules/redis.module.ts rename to apps/loadbalancer/src/modules/redis.module.ts diff --git a/packages/loadbalancer/test/app.e2e-spec.ts b/apps/loadbalancer/test/app.e2e-spec.ts similarity index 86% rename from packages/loadbalancer/test/app.e2e-spec.ts rename to apps/loadbalancer/test/app.e2e-spec.ts index c0fc4d5..f152734 100644 --- a/packages/loadbalancer/test/app.e2e-spec.ts +++ b/apps/loadbalancer/test/app.e2e-spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing' import { INestApplication } from '@nestjs/common' -import { LoadbalancerModule } from './../src/loadbalancer.module' +import { LoadbalancerModule } from '../src/loadbalancer.module' describe('AppController (e2e)', () => { let app: INestApplication diff --git a/packages/loadbalancer/test/jest-e2e.json b/apps/loadbalancer/test/jest-e2e.json similarity index 100% rename from packages/loadbalancer/test/jest-e2e.json rename to apps/loadbalancer/test/jest-e2e.json diff --git a/packages/loadbalancer/tsconfig.app.json b/apps/loadbalancer/tsconfig.app.json similarity index 78% rename from packages/loadbalancer/tsconfig.app.json rename to apps/loadbalancer/tsconfig.app.json index 2c2ecbb..1ff95da 100644 --- a/packages/loadbalancer/tsconfig.app.json +++ b/apps/loadbalancer/tsconfig.app.json @@ -2,7 +2,7 @@ "extends": "../../tsconfig.json", "compilerOptions": { "declaration": false, - "outDir": "../../dist/packages/loadbalancer" + "outDir": "../../dist/apps/loadbalancer" }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] diff --git a/packages/loadbalancer/tsconfig.build.json b/apps/loadbalancer/tsconfig.build.json similarity index 100% rename from packages/loadbalancer/tsconfig.build.json rename to apps/loadbalancer/tsconfig.build.json diff --git a/packages/loadbalancer/tsconfig.json b/apps/loadbalancer/tsconfig.json similarity index 100% rename from packages/loadbalancer/tsconfig.json rename to apps/loadbalancer/tsconfig.json diff --git a/packages/webrtc-server/Dockerfile b/apps/webrtc-server/Dockerfile similarity index 100% rename from packages/webrtc-server/Dockerfile rename to apps/webrtc-server/Dockerfile diff --git a/packages/webrtc-server/docker-compose-lb.yml b/apps/webrtc-server/docker-compose-lb.yml similarity index 100% rename from packages/webrtc-server/docker-compose-lb.yml rename to apps/webrtc-server/docker-compose-lb.yml diff --git a/packages/webrtc-server/docker-compose.yml b/apps/webrtc-server/docker-compose.yml similarity index 100% rename from packages/webrtc-server/docker-compose.yml rename to apps/webrtc-server/docker-compose.yml diff --git a/packages/webrtc-server/jest.config.ts b/apps/webrtc-server/jest.config.ts similarity index 100% rename from packages/webrtc-server/jest.config.ts rename to apps/webrtc-server/jest.config.ts diff --git a/packages/webrtc-server/nest-cli.json b/apps/webrtc-server/nest-cli.json similarity index 100% rename from packages/webrtc-server/nest-cli.json rename to apps/webrtc-server/nest-cli.json diff --git a/packages/webrtc-server/package.json b/apps/webrtc-server/package.json similarity index 93% rename from packages/webrtc-server/package.json rename to apps/webrtc-server/package.json index 935d4e8..728b3d5 100644 --- a/packages/webrtc-server/package.json +++ b/apps/webrtc-server/package.json @@ -8,11 +8,11 @@ "scripts": { "build": "nest build", "format": "prettier --write \"src/**/*.ts\"", - "check-types": "eslint \"{src,packages,libs,test}/**/*.ts\" --ignore-pattern demo/", + "check-types": "eslint \"{src,apps,libs,test}/**/*.ts\" --ignore-pattern demo/", "start": "nest start", "start:dev": "LOG_LEVEL=3 nest start --watch", "start:debug": "nest start --debug --watch", - "start:prod": "node dist/packages/webrtc-server/main", + "start:prod": "node dist/apps/webrtc-server/main", "lint": "eslint \"{src,test}/**/*.ts\" --fix", "test": "jest --config jest.config.ts", "test:watch": "jest --watch --config jest.config.ts", diff --git a/packages/webrtc-server/src/app.module.ts b/apps/webrtc-server/src/app.module.ts similarity index 100% rename from packages/webrtc-server/src/app.module.ts rename to apps/webrtc-server/src/app.module.ts diff --git a/packages/webrtc-server/src/config/app.config.ts b/apps/webrtc-server/src/config/app.config.ts similarity index 100% rename from packages/webrtc-server/src/config/app.config.ts rename to apps/webrtc-server/src/config/app.config.ts diff --git a/packages/webrtc-server/src/config/config.server.ts b/apps/webrtc-server/src/config/config.server.ts similarity index 99% rename from packages/webrtc-server/src/config/config.server.ts rename to apps/webrtc-server/src/config/config.server.ts index 60471ca..c2ab89a 100644 --- a/packages/webrtc-server/src/config/config.server.ts +++ b/apps/webrtc-server/src/config/config.server.ts @@ -17,6 +17,7 @@ export const config = { urls: `turn:${process.env.MEDIASOUP_CLIENT_ICESERVER_HOST}:${process.env.MEDIASOUP_CLIENT_ICESERVER_PORT}?transport=${process.env.MEDIASOUP_CLIENT_ICESERVER_PROTO}`, username: process.env.MEDIASOUP_CLIENT_ICESERVER_USER, credential: process.env.MEDIASOUP_CLIENT_ICESERVER_PASS, + credentialType: 'password', }, ], // mediasoup settings. diff --git a/packages/webrtc-server/src/config/constants.ts b/apps/webrtc-server/src/config/constants.ts similarity index 100% rename from packages/webrtc-server/src/config/constants.ts rename to apps/webrtc-server/src/config/constants.ts diff --git a/packages/webrtc-server/src/config/logger.config.ts b/apps/webrtc-server/src/config/logger.config.ts similarity index 100% rename from packages/webrtc-server/src/config/logger.config.ts rename to apps/webrtc-server/src/config/logger.config.ts diff --git a/packages/webrtc-server/src/lib/Room.ts b/apps/webrtc-server/src/lib/Room.ts similarity index 100% rename from packages/webrtc-server/src/lib/Room.ts rename to apps/webrtc-server/src/lib/Room.ts diff --git a/packages/webrtc-server/src/lib/RoomFactory.ts b/apps/webrtc-server/src/lib/RoomFactory.ts similarity index 100% rename from packages/webrtc-server/src/lib/RoomFactory.ts rename to apps/webrtc-server/src/lib/RoomFactory.ts diff --git a/packages/webrtc-server/src/lib/notification.service.ts b/apps/webrtc-server/src/lib/notification.service.ts similarity index 100% rename from packages/webrtc-server/src/lib/notification.service.ts rename to apps/webrtc-server/src/lib/notification.service.ts diff --git a/packages/webrtc-server/src/lib/room.interfaces.ts b/apps/webrtc-server/src/lib/room.interfaces.ts similarity index 100% rename from packages/webrtc-server/src/lib/room.interfaces.ts rename to apps/webrtc-server/src/lib/room.interfaces.ts diff --git a/packages/webrtc-server/src/main.ts b/apps/webrtc-server/src/main.ts similarity index 100% rename from packages/webrtc-server/src/main.ts rename to apps/webrtc-server/src/main.ts diff --git a/packages/webrtc-server/src/modules/redis.module.ts b/apps/webrtc-server/src/modules/redis.module.ts similarity index 100% rename from packages/webrtc-server/src/modules/redis.module.ts rename to apps/webrtc-server/src/modules/redis.module.ts diff --git a/packages/webrtc-server/src/rooms/dto/rooms.dto.ts b/apps/webrtc-server/src/rooms/dto/rooms.dto.ts similarity index 100% rename from packages/webrtc-server/src/rooms/dto/rooms.dto.ts rename to apps/webrtc-server/src/rooms/dto/rooms.dto.ts diff --git a/packages/webrtc-server/src/rooms/rooms.controller.spec.ts b/apps/webrtc-server/src/rooms/rooms.controller.spec.ts similarity index 100% rename from packages/webrtc-server/src/rooms/rooms.controller.spec.ts rename to apps/webrtc-server/src/rooms/rooms.controller.spec.ts diff --git a/packages/webrtc-server/src/rooms/rooms.controller.ts b/apps/webrtc-server/src/rooms/rooms.controller.ts similarity index 100% rename from packages/webrtc-server/src/rooms/rooms.controller.ts rename to apps/webrtc-server/src/rooms/rooms.controller.ts diff --git a/packages/webrtc-server/src/rooms/rooms.module.ts b/apps/webrtc-server/src/rooms/rooms.module.ts similarity index 100% rename from packages/webrtc-server/src/rooms/rooms.module.ts rename to apps/webrtc-server/src/rooms/rooms.module.ts diff --git a/packages/webrtc-server/src/rooms/rooms.service.spec.ts b/apps/webrtc-server/src/rooms/rooms.service.spec.ts similarity index 100% rename from packages/webrtc-server/src/rooms/rooms.service.spec.ts rename to apps/webrtc-server/src/rooms/rooms.service.spec.ts diff --git a/packages/webrtc-server/src/rooms/rooms.service.ts b/apps/webrtc-server/src/rooms/rooms.service.ts similarity index 100% rename from packages/webrtc-server/src/rooms/rooms.service.ts rename to apps/webrtc-server/src/rooms/rooms.service.ts diff --git a/packages/webrtc-server/test/app.e2e-spec.ts b/apps/webrtc-server/test/app.e2e-spec.ts similarity index 100% rename from packages/webrtc-server/test/app.e2e-spec.ts rename to apps/webrtc-server/test/app.e2e-spec.ts diff --git a/packages/webrtc-server/test/jest-e2e.json b/apps/webrtc-server/test/jest-e2e.json similarity index 100% rename from packages/webrtc-server/test/jest-e2e.json rename to apps/webrtc-server/test/jest-e2e.json diff --git a/packages/webrtc-server/tsconfig.app.json b/apps/webrtc-server/tsconfig.app.json similarity index 78% rename from packages/webrtc-server/tsconfig.app.json rename to apps/webrtc-server/tsconfig.app.json index 5f003fb..960f906 100644 --- a/packages/webrtc-server/tsconfig.app.json +++ b/apps/webrtc-server/tsconfig.app.json @@ -2,7 +2,7 @@ "extends": "../../tsconfig.json", "compilerOptions": { "declaration": false, - "outDir": "../../dist/packages/webrtc-server" + "outDir": "../../dist/apps/webrtc-server" }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] diff --git a/packages/webrtc-server/tsconfig.build.json b/apps/webrtc-server/tsconfig.build.json similarity index 100% rename from packages/webrtc-server/tsconfig.build.json rename to apps/webrtc-server/tsconfig.build.json diff --git a/packages/webrtc-server/tsconfig.json b/apps/webrtc-server/tsconfig.json similarity index 100% rename from packages/webrtc-server/tsconfig.json rename to apps/webrtc-server/tsconfig.json diff --git a/packages/webrtc-server/yarn.lock b/apps/webrtc-server/yarn.lock similarity index 100% rename from packages/webrtc-server/yarn.lock rename to apps/webrtc-server/yarn.lock diff --git a/jest.config.ts b/jest.config.ts index 773642b..50e436a 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -2,7 +2,7 @@ import type { Config } from 'jest' const config: Config = { moduleFileExtensions: ['js', 'json', 'ts'], - rootDir: './packages/webrtc-server', + rootDir: './apps/webrtc-server', testRegex: '.*\\.spec\\.ts$', transform: { '^.+\\.(t|j)s$': 'ts-jest', diff --git a/nest-cli.json b/nest-cli.json index 372be66..d14a960 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -1,31 +1,31 @@ { "$schema": "https://json.schemastore.org/nest-cli", "collection": "@nestjs/schematics", - "sourceRoot": "packages/webrtc-server/src", + "sourceRoot": "apps/webrtc-server/src", "compilerOptions": { "deleteOutDir": true, "webpack": true, - "tsConfigPath": "packages/webrtc-server/tsconfig.app.json" + "tsConfigPath": "apps/webrtc-server/tsconfig.app.json" }, "monorepo": true, - "root": "packages/webrtc-server", + "root": "apps/webrtc-server", "projects": { "webrtc-server": { "type": "application", - "root": "packages/webrtc-server", + "root": "apps/webrtc-server", "entryFile": "main", - "sourceRoot": "packages/webrtc-server/src", + "sourceRoot": "apps/webrtc-server/src", "compilerOptions": { - "tsConfigPath": "packages/webrtc-server/tsconfig.app.json" + "tsConfigPath": "apps/webrtc-server/tsconfig.app.json" } }, "loadbalancer": { "type": "application", - "root": "packages/loadbalancer", + "root": "apps/loadbalancer", "entryFile": "main", - "sourceRoot": "packages/loadbalancer/src", + "sourceRoot": "apps/loadbalancer/src", "compilerOptions": { - "tsConfigPath": "packages/loadbalancer/tsconfig.app.json" + "tsConfigPath": "apps/loadbalancer/tsconfig.app.json" } } } diff --git a/package.json b/package.json index 38f4cc8..04ac410 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "private": true, "license": "Apache-2.0", "workspaces": [ - "packages/*" + "apps/*" ], "repository": { "url": "https://github.com/2060-io/webrtc-server", @@ -14,18 +14,18 @@ }, "scripts": { "build": "nest build", - "format": "prettier --write \"packages/**/*.ts\"", - "check-types": "eslint \"{src,packages,libs,test}/**/*.ts\" --ignore-pattern demo/", + "format": "prettier --write \"apps/**/*.ts\"", + "check-types": "eslint \"{src,apps,libs,test}/**/*.ts\" --ignore-pattern demo/", "start": "nest start", "start:dev": "LOG_LEVEL=3 nest start --watch", "start:debug": "nest start --debug --watch", - "start:prod": "node dist/packages/webrtc-server/main", + "start:prod": "node dist/apps/webrtc-server/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest --config jest.config.ts", "test:watch": "jest --watch --config jest.config.ts", "test:cov": "jest --coverage --config jest.config.ts", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand --config jest.config.ts", - "test:e2e": "jest --config ./packages/webrtc-server/test/jest-e2e.json" + "test:e2e": "jest --config ./apps/webrtc-server/test/jest-e2e.json" }, "dependencies": { "@nestjs-modules/ioredis": "^2.0.2", diff --git a/release-please-config.json b/release-please-config.json index e5b549d..847c792 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -6,10 +6,10 @@ "bump-minor-pre-major": true, "bump-patch-for-minor-pre-major": true, "packages": { - "packages/loadbalancer": { + "apps/loadbalancer": { "release-type": "node" }, - "packages/webrtc-server": { + "apps/webrtc-server": { "release-type": "node" } }, From 086f6d73b69b16dacabfff65183c1dfb2410979c Mon Sep 17 00:00:00 2001 From: Gabriel Mata Date: Wed, 5 Feb 2025 15:39:55 -0500 Subject: [PATCH 10/15] fix: Remove binary of pymediasoup demo --- .../src/__pycache__/mediasoup.cpython-310.pyc | Bin 11829 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 demo/pymediasoup-client/src/__pycache__/mediasoup.cpython-310.pyc diff --git a/demo/pymediasoup-client/src/__pycache__/mediasoup.cpython-310.pyc b/demo/pymediasoup-client/src/__pycache__/mediasoup.cpython-310.pyc deleted file mode 100644 index 01affc4eaa2dc275f9939b461c37a3e16cd9eef2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11829 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 u Date: Thu, 6 Feb 2025 08:35:23 -0500 Subject: [PATCH 11/15] docs: Refactor documentation and fix following the recommendations suggested --- README.md | 252 ++---------------- apps/loadbalancer/README.md | 86 +++--- .../docs/health-check-process.png | Bin 0 -> 24183 bytes .../docs/room-allocation-flow.png | Bin 0 -> 40003 bytes apps/loadbalancer/docs/room-closure-flow.png | Bin 0 -> 27664 bytes .../loadbalancer/docs/server-registration.png | Bin 0 -> 23593 bytes .../src/loadbalancer.controller.spec.ts | 22 -- apps/webrtc-server/README.md | 149 +++++++++++ {docs => apps/webrtc-server/docs}/image.png | Bin .../docs}/mediasoup-server-protocol.md | 0 .../docs}/webrtc-client-setup-guide.md | 4 +- 11 files changed, 216 insertions(+), 297 deletions(-) create mode 100644 apps/loadbalancer/docs/health-check-process.png create mode 100644 apps/loadbalancer/docs/room-allocation-flow.png create mode 100644 apps/loadbalancer/docs/room-closure-flow.png create mode 100644 apps/loadbalancer/docs/server-registration.png delete mode 100644 apps/loadbalancer/src/loadbalancer.controller.spec.ts create mode 100644 apps/webrtc-server/README.md rename {docs => apps/webrtc-server/docs}/image.png (100%) rename {docs => apps/webrtc-server/docs}/mediasoup-server-protocol.md (100%) rename {docs => apps/webrtc-server/docs}/webrtc-client-setup-guide.md (99%) diff --git a/README.md b/README.md index 8fd38e3..a029bb3 100644 --- a/README.md +++ b/README.md @@ -1,248 +1,30 @@ -# WebRTC Server with Mediasoup, Docker, Kubernetes and Turn Server +# WebRTC Services 2060.io -This application, based on [Mediasoup-demo v3](https://github.com/versatica/mediasoup-demo/tree/v3), has been extensively modified and customized for 2060 to include a TURN server, specifically Coturn. +This monorepo provides a robust and scalable infrastructure for `real-time WebRTC communications`, designed and implemented by **2060.io**. It includes **WebRTC services** and a **load balancer** to efficiently manage multiple server instances, ensuring optimal resource distribution and high availability. -You can deploy it using Docker or Kubernetes, with integration of the [Coturn](https://github.com/coturn/coturn) server for TURN functionality. +## Included Applications -## Table of Contents +### **WebRTC Server** -- [WebRTC Server with Mediasoup, Docker, Kubernetes and Turn Server](#webrtc-server-with-mediasoup-docker-kubernetes-and-turn-server) - - [Table of Contents](#table-of-contents) - - [Pre-requisites](#pre-requisites) - - [Environment Variables](#environment-variables) - - [Diagram of solution webrtc-server](#diagram-of-solution-webrtc-server) - - [Changing the TCP Port (Web App and WSS)](#changing-the-tcp-port-web-app-and-wss) - - [Docker build](#docker-build) - - [Docker Running](#docker-running) - - [Kubernetes Running](#kubernetes-running) - - [WebRTC Server API](#webrtc-server-api) - - [GetRoomId](#get-room-id) - - [Create Rooms](#create-rooms) - - [Config](#config) - - [Other Server Mediasoup Endpoints](#other-server-mediasoup-endpoints) - - [ICE Server Configuration](#ice-server-configuration) - - [Protocol Documentation Webrtc-Server](#protocol-documentation-webrtc-server) - - [WebRTC Client Setup Documentation](#webrtc-client-setup-documentation) +A highly customized WebRTC server based on **Mediasoup-demo v3**, tailored to support **2060.io** services. It provides scalable, low-latency audio/video streaming with advanced WebRTC capabilities. -## Pre-requisites +**Key Features:** -- A Linux server with a public IP address (or an EIP on AWS) -- Docker and Docker Compose, for local deployment -- A TURN server, with port 3478 forwarded from the public IP (configured within Docker) -- A Mediasoup server running in a container with port 443 forwarded from the public IP (this is configured in docker) +- Multi-peer WebRTC communication +- Scalable deployment with Kubernetes & Docker -## Enviroment Variables +**Documentation**: [Read more](./apps/webrtc-server/README.md) -To configure and build the `ICE Server` you can be use following enviroment variables: +--- -| Variable | Description | Default Value | -| ---------------------------------- | ----------------------------------------------- | ------------- | -| `MEDIASOUP_CLIENT_PROTOOPORT` | Port used for the connection. | `443` | -| `MEDIASOUP_CLIENT_ICESERVER_PROTO` | Protocol configuration used (e.g., `udp`). | `udp` | -| `MEDIASOUP_CLIENT_ICESERVER_PORT` | Port set in the TURN server to receive traffic. | `3478` | -| `MEDIASOUP_CLIENT_ICESERVER_USER` | Username for the TURN server. | | -| `MEDIASOUP_CLIENT_ICESERVER_PASS` | Password for the TURN server. | | -| `MEDIASOUP_CLIENT_ICESERVER_HOST` | Public IP address of the TURN server. | | +### ** Load Balancer for WebRTC Services** -Additional variables for configuring the `webrtc-server`: +A specialized load balancer designed to **distribute WebRTC sessions** across multiple Mediasoup-based servers, enhancing scalability and fault tolerance. -| Variable | Description | Default Value | -| ------------------------ | -------------------------------------------------------------------------------------------------------- | ----------------------------------- | -| `PROTOO_LISTEN_PORT` | Port for the protoo WebSocket server and HTTP API server. | `4443` | -| `HTTPS_CERT_FULLCHAIN` | Path to the fullchain certificate file for HTTPS. | `/certs/fullchain.pem` | -| `HTTPS_CERT_PRIVKEY` | Path to the private key file for HTTPS. | `/certs/privkey.pem` | -| `MEDIASOUP_INGRESS_HOST` | Ingress host for the mediasoup client. | | -| `MEDIASOUP_MIN_PORT` | Minimum port for RTC connections in mediasoup. | `40000` | -| `MEDIASOUP_MAX_PORT` | Maximum port for RTC connections in mediasoup. | `49999` | -| `MEDIASOUP_LISTEN_IP` | The listening IP for audio/video in mediasoup. | `0.0.0.0` or `127.0.0.1` | -| `MEDIASOUP_ANNOUNCED_IP` | Public IP address for audio/video in mediasoup.. | | -| `MEDIASOUP_INGRESS_HOST` | Set Ingress host for /rooms response | | -| `LOADBALANCER_URL` | Specifies the URL of the load balancer responsible for distributing WebRTC rooms among available servers | | -| `SERVICE_URL` | Defines the base URL of the WebRTC server that registers itself with the load balancer | | +**Key Features:** -## Diagram of solution webrtc-server +- **Intelligent session routing** based on server capacity +- **Automatic server registration and monitoring** +- **Dynamic load distribution** for high availability -![Solution Architecture](docs/image.png) - -## Changing the TCP Port (Web App and WSS) - -`Server`, The port is configured in `server/config.js` with `PROTOO_LISTEN_PORT`. - -``` -git clone https://github.com/2060-io/webrtc-server.git -vi docker-compose.yml -services: - mediasoup: - image: webrtc-server:test - environment: - PROTOO_LISTEN_PORT: 443 - ports: - - '443:443' -``` - -:exclamation: if you change this you have to rebuild the Docker image - -## Docker build - -- Clone the repo -- Run docker build in the server folder - -``` -git clone https://github.com/2060-io/webrtc-server.git -cd package/webrtc-server -docker build . -t 2060-webrtc-server:test -``` - -Should the start.sh script fail to detect the container's IP address, you may modify the Dockerfile by replacing CMD ["sh", "/service/start.sh"] with CMD ["node", "/service/server.js"]. Subsequently, manually set the MEDIASOUP_ANNOUNCED_IP variable. - -You are required to generate a certificate and place both its public and private part in PEM format in a directory reachable by the application. The full path must be set on `HTTPS_CERT_FULLCHAIN` and `HTTPS_CERT_PRIVKEY` environment variable respectively. - -Certificate can be created using OpenSSL with the execution of the following commands: - -```bash -openssl genpkey -algorithm RSA -out privkey.pem -openssl req -new -key privkey.pem -out request.csr -openssl x509 -req -in request.csr -signkey privkey.pem -out fullchain.pem -``` - -## Docker Running - -Edit `docker-compose.yml` to update environment variables and Docker image settings for mediasoup with your own (otherwise it will use my turn server and it will not work) - -```bash -docker-compose up -``` - -## Kubernetes Running - -**`IMPORTANT`**, Ensure the Kubernetes load balancer allows UDP traffic to the Coturn service nodes. Set the public IP in the `.env` file as `MEDIASOUP_CLIENT_ICESERVER_HOST` enviroment variable. - -## WebRTC Server API - -### Create Rooms - -This endpoint is used to create a new room or use an existing room based on the provided or generated `roomId`. It returns the WebSocket URL and the `roomId` used, and checks for unique room identifiers before proceeding. - -#### Request - -- Method: POST -- Endpoint: `/rooms/:roomId?` -- Port: 443 -- Body: - -```json -{ - "eventNotificationUri": "http://example.com/notification", - "maxPeerCount": 50 -} -``` - -#### Parameters (all optional) - -- `roomId`: It allows you to send the code for a room that needs to be created. If no code is sent, the endpoint will automatically create one, which will be returned by the method. - -- `eventNotificationUri`: Enable the sending of notifications when a peer joins or leaves the room, The responses that the configured endpoint will receive are the following: - -```json -{ - "roomId": "12345678", - "peerId": "peerId1234", - "event": "peer-joined" || "peer-left", -} -``` - -- `maxPeerCount`: Allows set the maximum number of allowed participants into the room - -#### Response - -- Status Code: 200 (OK) -- Body: - -```json -{ - "protocol": "2060-mediasoup-v1", - "wsUrl": "wss://localhost:443", - "roomId": "12345abcde" -} -``` - -**Note:** With the response obtained, the client must add the necessary parameters to complete the connection.. example `wss://localhost:443/?roomId=12345abcde&peerId=4321` or `wss://localhost:443/?roomId=12345abcde` - -#### Error - -- Status Code: 500 (Internal Server Error) -- Body: - -```json -{ - "error": "Room with roomId 12345abcde already exists." -} -``` - -### Config - -Provides the current configuration of the server as defined in the `config.js` file. - -#### Request - -- Method: GET -- Endpoint: `/config` -- Port: 443 - -#### Response - -- Status Code: 200 (OK) -- Body example: - -```json -{ - "config": { - "https": { - "listenPort": 443, - "listenIp": "localhost", - "tls": { - "cert": "/path/to/cert.pem", - "key": "/path/to/key.pem" - } - } - } -} -``` - -### Other Server Mediasoup Endpoints - -Comprehensive endpoints for various functionalities: - -- `/rooms/:roomId`: `GET`Returns the RTP capabilities of the mediasoup router for a specific room. -- `/rooms/:roomId/broadcasters`: `POST` Creates a new broadcaster in a specific room. -- `/rooms/:roomId/broadcasters/:broadcasterId`: `DELETE` Deletes a broadcaster from a specific room. -- `/rooms/:roomId/broadcasters/:broadcasterId/transports`: `POST` Creates a new transport associated with a broadcaster. -- `/rooms/:roomId/broadcasters/:broadcasterId/transports/:transportId/connect`: `POST` Connects a transport belonging to a broadcaster. -- `/rooms/:roomId/broadcasters/:broadcasterId/transports/:transportId/producers`: `POST` Creates a new producer associated with a broadcaster. -- `/rooms/:roomId/broadcasters/:broadcasterId/transports/:transportId/consume`: `POST` Creates a new consumer associated with a broadcaster. -- `/rooms/:roomId/broadcasters/:broadcasterId/transports/:transportId/consume/data`: `POST` Creates a new data consumer associated with a broadcaster. -- `/rooms/:roomId/broadcasters/:broadcasterId/transports/:transportId/produce/data`: `POST` Creates a new data producer associated with a broadcaster. - -## ICE Server Configuration - -Retrieve ICE Server settings via a peer websocket request to create a WebRTC Transport `createWebRtcTransport`: - -```json -{ - "iceServers": [ - { - "urls": "turn:localhost:3478?transport=udp", - "username": "test", - "credential": "test123" - } - ] -} -``` - -## Protocol Documentation Webrtc-Server - -For detailed information on Mediasoup Server protocol and request methods, check out the [Mediasoup Server Protocol Guide](./docs/mediasoup-server-protocol.md). - -## WebRTC Client Setup Documentation - -For detailed instructions on setting up a WebRTC client using Mediasoup, including JavaScript and Python examples, check out the [WebRTC Client Setup Guide](./docs/webrtc-client-setup-guide.md). +**Documentation**: [Read more](./apps/loadbalancer/README.md) diff --git a/apps/loadbalancer/README.md b/apps/loadbalancer/README.md index e767ec9..0523621 100644 --- a/apps/loadbalancer/README.md +++ b/apps/loadbalancer/README.md @@ -1,12 +1,12 @@ # Load Balancer for WebRTC Mediasoup Servers -## 🚀 Overview +## Overview This project implements a **load balancer** for WebRTC **Mediasoup** servers. It enables **automatic server registration**, **health monitoring**, and **dynamic load balancing** to efficiently distribute WebRTC rooms among the available servers, ensuring a scalable and robust architecture. --- -## ⚡ **Key Features** +## **Key Features** ### **Server Registration** @@ -36,9 +36,49 @@ This project implements a **load balancer** for WebRTC **Mediasoup** servers. It --- -## 📌 **Endpoints** +## **Architecture & Implementation** -### ✅ **Register a WebRTC Server** +### **1. Server Registration Flow** + +1. A Mediasoup server sends a `POST /register` request. +2. If the server is already registered, its entry is removed and re-added. +3. The server’s total capacity is calculated (`workers × 500` consumers). +4. The server is stored in Redis with `health: true`. + +![Server Registration](docs/server-registration.png) + +### **2. Health Check Process** + +1. Every **30 seconds** (or as defined in `HEALTH_CHECK_INTERVAL`), the system checks all registered servers. +2. A `GET /health` request is sent to each server. +3. If the server is **healthy**, its `health` status remains `true`. +4. If the server is **unhealthy**, its `health` status is updated to `false`. + +![Health Check Process](docs/health-check-process.png) + +### **3. Room Allocation Flow** + +1. A client requests to **create a room** (`POST rooms/:roomId?`). +2. The load balancer filters out **unhealthy servers**. +3. The server with the **highest available capacity** is selected. +4. The expected consumers for the room are **subtracted** from the server’s capacity. +5. The room is created, and the WebSocket URL is returned. + +![Room Allocation Flow](docs/room-allocation-flow.png) + +### **4. Room Closure Flow** + +1. When a room is **closed** within the WebRTC server and the load balancer mode is active, the server sends a `POST /room-closed` notification (webhook) to the load balancer, informing that the roomId has been closed. +2. The number of consumers freed is **added back** to the server’s available capacity. +3. The room entry is **deleted** from Redis. + +![Room Closure Flow](docs/room-closure-flow.png) + +--- + +## **Endpoints** + +### **Register a WebRTC Server** Registers a new Mediasoup server, calculating its total capacity. @@ -62,7 +102,7 @@ Registers a new Mediasoup server, calculating its total capacity. --- -### ✅ **Create or Retrieve a Room** +### **Create Room** Selects the most capable healthy server and assigns the room. @@ -89,7 +129,7 @@ Selects the most capable healthy server and assigns the room. --- -### ✅ **Notify Room Closure** +### **Notify Room Closure** Updates the server's available capacity when a room is closed. @@ -114,7 +154,7 @@ Updates the server's available capacity when a room is closed. --- -## ⚙️ **Configuration** +## **Configuration** The application uses **environment variables** for configuration. @@ -127,38 +167,6 @@ The application uses **environment variables** for configuration. --- -## 🏗️ **Architecture & Implementation** - -### 🔹 **1. Server Registration Flow** - -1. A Mediasoup server sends a `POST /register` request. -2. If the server is already registered, its entry is removed and re-added. -3. The server’s total capacity is calculated (`workers × 500` consumers). -4. The server is stored in Redis with `health: true`. - -### 🔹 **2. Health Check Process** - -1. Every **30 seconds** (or as defined in `HEALTH_CHECK_INTERVAL`), the system checks all registered servers. -2. A `GET /health` request is sent to each server. -3. If the server is **healthy**, its `health` status remains `true`. -4. If the server is **unhealthy**, its `health` status is updated to `false`. - -### 🔹 **3. Room Allocation Flow** - -1. A client requests to **create a room** (`POST rooms/:roomId?`). -2. The load balancer filters out **unhealthy servers**. -3. The server with the **highest available capacity** is selected. -4. The expected consumers for the room are **subtracted** from the server’s capacity. -5. The room is created, and the WebSocket URL is returned. - -### 🔹 **4. Room Closure Flow** - -1. When a room is **closed**, a `POST /room-closed` request is sent. -2. The number of consumers freed is **added back** to the server’s available capacity. -3. The room entry is **deleted** from Redis. - ---- - ## 🔧 **Deployment & Scaling** ### 1️ **Running with Docker** diff --git a/apps/loadbalancer/docs/health-check-process.png b/apps/loadbalancer/docs/health-check-process.png new file mode 100644 index 0000000000000000000000000000000000000000..1e3e04fd38d874871d666e4234b6d7fbe69a468d GIT binary patch literal 24183 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 i;LYExN=McHA8kPWr@f(Wb8|}tjQ7~ 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 literal 0 HcmV?d00001 diff --git a/apps/loadbalancer/src/loadbalancer.controller.spec.ts b/apps/loadbalancer/src/loadbalancer.controller.spec.ts deleted file mode 100644 index cf1f6c9..0000000 --- a/apps/loadbalancer/src/loadbalancer.controller.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing' -import { LoadbalancerController } from './loadbalancer.controller' -import { LoadbalancerService } from './loadbalancer.service' - -describe('LoadbalancerController', () => { - let loadbalancerController: LoadbalancerController - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [LoadbalancerController], - providers: [LoadbalancerService], - }).compile() - - loadbalancerController = app.get(LoadbalancerController) - }) - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(loadbalancerController.getHello()).toBe('Hello World!') - }) - }) -}) diff --git a/apps/webrtc-server/README.md b/apps/webrtc-server/README.md new file mode 100644 index 0000000..adfbcfc --- /dev/null +++ b/apps/webrtc-server/README.md @@ -0,0 +1,149 @@ +# WebRTC Server with Mediasoup, Docker, Kubernetes, and Turn Server + +This application is based on [Mediasoup-demo v3](https://github.com/versatica/mediasoup-demo/tree/v3) and has been modified and customized for 2060.io project. + +You can deploy it using Docker or Kubernetes, with integration of the [Coturn](https://github.com/coturn/coturn) server for TURN functionality. + +## Table of Contents + +- [Pre-requisites](#pre-requisites) +- [Environment Variables](#environment-variables) +- [Solution Architecture Diagram](#solution-architecture-diagram) +- [Configuring TCP Port (Web App and WSS)](#configuring-tcp-port-web-app-and-wss) +- [Docker Build and Deployment](#docker-build-and-deployment) +- [Kubernetes Deployment](#kubernetes-deployment) +- [WebRTC Server API](#webrtc-server-api) +- [ICE Server Configuration](#ice-server-configuration) +- [Protocol Documentation](#protocol-documentation) +- [WebRTC Client Setup](#webrtc-client-setup) + +## Pre-requisites + +- A Linux server with a public IP address (or an EIP on AWS) +- Docker and Docker Compose for local deployment +- A TURN server with port `3478` forwarded from the public IP (configured within Docker) +- A Mediasoup server running in a container with port `443` forwarded from the public IP + +## Environment Variables + +Enviroment variables for configuring the `webrtc-server`: + +| Variable | Description | Default Value | +| ------------------------ | -------------------------------------------------------------------------------------------------------- | ----------------------------------- | +| `PROTOO_LISTEN_PORT` | Port for the protoo WebSocket server and HTTP API server. | `4443` | +| `HTTPS_CERT_FULLCHAIN` | Path to the fullchain certificate file for HTTPS. | `/certs/fullchain.pem` | +| `HTTPS_CERT_PRIVKEY` | Path to the private key file for HTTPS. | `/certs/privkey.pem` | +| `MEDIASOUP_INGRESS_HOST` | Ingress host for the mediasoup client. | | +| `MEDIASOUP_MIN_PORT` | Minimum port for RTC connections in mediasoup. | `40000` | +| `MEDIASOUP_MAX_PORT` | Maximum port for RTC connections in mediasoup. | `49999` | +| `MEDIASOUP_LISTEN_IP` | The listening IP for audio/video in mediasoup. | `0.0.0.0` or `127.0.0.1` | +| `MEDIASOUP_ANNOUNCED_IP` | Public IP address for audio/video in mediasoup.. | | +| `MEDIASOUP_INGRESS_HOST` | Set Ingress host for /rooms response | | +| `LOADBALANCER_URL` | Specifies the URL of the load balancer responsible for distributing WebRTC rooms among available servers | | +| `SERVICE_URL` | Defines the base URL of the WebRTC server that registers itself with the load balancer | | + +To configure and build the `ICE Server`, you can use the following environment variables: + +| Variable | Description | Default Value | +| ---------------------------------- | ------------------------------------- | ------------- | +| `MEDIASOUP_CLIENT_PROTOOPORT` | Port used for the connection. | `443` | +| `MEDIASOUP_CLIENT_ICESERVER_PROTO` | Protocol configuration used (`udp`). | `udp` | +| `MEDIASOUP_CLIENT_ICESERVER_PORT` | Port set in the TURN server. | `3478` | +| `MEDIASOUP_CLIENT_ICESERVER_USER` | Username for the TURN server. | | +| `MEDIASOUP_CLIENT_ICESERVER_PASS` | Password for the TURN server. | | +| `MEDIASOUP_CLIENT_ICESERVER_HOST` | Public IP address of the TURN server. | | + +## Solution Architecture Diagram + +![Solution Architecture](docs/image.png) + +## Configuring TCP Port (Web App and WSS) + +The `PROTOO_LISTEN_PORT` environment variable in `docker-compose.yml` defines the listening port: + +```yaml +services: + mediasoup: + environment: + PROTOO_LISTEN_PORT: 443 + ports: + - '443:4443' +``` + +:exclamation: If you change this value, you must rebuild the Docker image. + +## Docker Build and Deployment + +Clone the repository and build the Docker image: + +```sh +git clone https://github.com/2060-io/webrtc-server.git +cd package/webrtc-server +docker build . -t 2060-webrtc-server:test +``` + +Run the application with Docker Compose: + +```sh +docker-compose up +``` + +## Kubernetes Deployment + +Ensure that the Kubernetes load balancer allows UDP traffic to the Coturn service nodes. Set the public IP in the `.env` file as `MEDIASOUP_CLIENT_ICESERVER_HOST`. + +## WebRTC Server API + +### Create Rooms + +Creates a new room or retrieves an existing one. + +**Request:** + +- **Method:** `POST` +- **Endpoint:** `/rooms/:roomId?` +- **Port:** `443` +- **Body:** + +```json +{ + "eventNotificationUri": "http://example.com/notification", + "maxPeerCount": 50 +} +``` + +**Response:** + +```json +{ + "protocol": "2060-mediasoup-v1", + "wsUrl": "wss://localhost:443", + "roomId": "12345abcde" +} +``` + +**Also you can use swagger documentation of WebRTC server API visit url:** `https://yourserver-ip/API`. + +## ICE Server Configuration + +ICE Server settings for WebRTC transport creation: + +```json +{ + "iceServers": [ + { + "urls": "turn:localhost:3478?transport=udp", + "username": "test", + "credential": "test123" + } + ] +} +``` + +## Protocol Documentation + +Refer to the [Mediasoup Server Protocol Guide](./docs/mediasoup-server-protocol.md) for details on Mediasoup's request handling. + +## WebRTC Client Setup + +Check the [WebRTC Client Setup Guide](./docs/webrtc-client-setup-guide.md) for instructions on setting up a Mediasoup WebRTC client. diff --git a/docs/image.png b/apps/webrtc-server/docs/image.png similarity index 100% rename from docs/image.png rename to apps/webrtc-server/docs/image.png diff --git a/docs/mediasoup-server-protocol.md b/apps/webrtc-server/docs/mediasoup-server-protocol.md similarity index 100% rename from docs/mediasoup-server-protocol.md rename to apps/webrtc-server/docs/mediasoup-server-protocol.md diff --git a/docs/webrtc-client-setup-guide.md b/apps/webrtc-server/docs/webrtc-client-setup-guide.md similarity index 99% rename from docs/webrtc-client-setup-guide.md rename to apps/webrtc-server/docs/webrtc-client-setup-guide.md index 7165ac4..04edc6d 100644 --- a/docs/webrtc-client-setup-guide.md +++ b/apps/webrtc-server/docs/webrtc-client-setup-guide.md @@ -350,4 +350,6 @@ With this method, the Mediasoup server ensures efficient handling of call termin ## Conclusion By following this guide, you will have a fully functional WebRTC client implemented in both JavaScript and Python, -using Mediasoup for media transport and WebSocket signaling. \ No newline at end of file +using Mediasoup for media transport and WebSocket signaling. + + From d5bf6fd243a515a489042d4f5c7cf1fa00999292 Mon Sep 17 00:00:00 2001 From: Gabriel Mata Date: Thu, 6 Feb 2025 08:47:06 -0500 Subject: [PATCH 12/15] fix: format prettier scripts global --- apps/loadbalancer/package.json | 1 - apps/webrtc-server/package.json | 1 - package.json | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/loadbalancer/package.json b/apps/loadbalancer/package.json index 9a6d321..6043cb8 100644 --- a/apps/loadbalancer/package.json +++ b/apps/loadbalancer/package.json @@ -7,7 +7,6 @@ "license": "Apache-2.0", "scripts": { "build": "nest build", - "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", diff --git a/apps/webrtc-server/package.json b/apps/webrtc-server/package.json index 728b3d5..17f7157 100644 --- a/apps/webrtc-server/package.json +++ b/apps/webrtc-server/package.json @@ -7,7 +7,6 @@ "license": "Apache-2.0", "scripts": { "build": "nest build", - "format": "prettier --write \"src/**/*.ts\"", "check-types": "eslint \"{src,apps,libs,test}/**/*.ts\" --ignore-pattern demo/", "start": "nest start", "start:dev": "LOG_LEVEL=3 nest start --watch", diff --git a/package.json b/package.json index 04ac410..a45a83c 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ }, "scripts": { "build": "nest build", - "format": "prettier --write \"apps/**/*.ts\"", + "format": "prettier --write \"apps/**/src/**/*.ts\" \"apps/**/test/**/*.ts\"", "check-types": "eslint \"{src,apps,libs,test}/**/*.ts\" --ignore-pattern demo/", "start": "nest start", "start:dev": "LOG_LEVEL=3 nest start --watch", From d7f138efe60a5c437fa984174f4c905b1efed6d2 Mon Sep 17 00:00:00 2001 From: Gabriel Mata Date: Thu, 6 Feb 2025 15:49:25 -0500 Subject: [PATCH 13/15] docs: add plantuml files to docs --- apps/loadbalancer/README.md | 2 +- apps/loadbalancer/docs/health-check-process.puml | 13 +++++++++++++ apps/loadbalancer/docs/room-allocation-flow.puml | 13 +++++++++++++ apps/loadbalancer/docs/room-closure-flow.puml | 11 +++++++++++ ...gistration.png => server-registration-flow.png} | Bin .../docs/server-registration-flow.puml | 13 +++++++++++++ 6 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 apps/loadbalancer/docs/health-check-process.puml create mode 100644 apps/loadbalancer/docs/room-allocation-flow.puml create mode 100644 apps/loadbalancer/docs/room-closure-flow.puml rename apps/loadbalancer/docs/{server-registration.png => server-registration-flow.png} (100%) create mode 100644 apps/loadbalancer/docs/server-registration-flow.puml diff --git a/apps/loadbalancer/README.md b/apps/loadbalancer/README.md index 0523621..46224c9 100644 --- a/apps/loadbalancer/README.md +++ b/apps/loadbalancer/README.md @@ -45,7 +45,7 @@ This project implements a **load balancer** for WebRTC **Mediasoup** servers. It 3. The server’s total capacity is calculated (`workers × 500` consumers). 4. The server is stored in Redis with `health: true`. -![Server Registration](docs/server-registration.png) +![Server Registration](docs/server-registration-flow.png) ### **2. Health Check Process** diff --git a/apps/loadbalancer/docs/health-check-process.puml b/apps/loadbalancer/docs/health-check-process.puml new file mode 100644 index 0000000..5b48842 --- /dev/null +++ b/apps/loadbalancer/docs/health-check-process.puml @@ -0,0 +1,13 @@ +@startuml +title Health Check Process + +Loop Every 30 seconds (HEALTH_CHECK_INTERVAL) + LoadBalancer -> Webrtc-Server: GET /health + alt Server is healthy + LoadBalancer -> Redis: Update health: true + else Server is unhealthy + LoadBalancer -> Redis: Update health: false + end +end + +@enduml diff --git a/apps/loadbalancer/docs/room-allocation-flow.puml b/apps/loadbalancer/docs/room-allocation-flow.puml new file mode 100644 index 0000000..07150be --- /dev/null +++ b/apps/loadbalancer/docs/room-allocation-flow.puml @@ -0,0 +1,13 @@ +@startuml +title Room Allocation Flow + + +Agent -> LoadBalancer: POST /rooms/:roomId? +LoadBalancer -> Redis: Fetch list of available servers +LoadBalancer -> LoadBalancer: Filter out unhealthy servers +LoadBalancer -> LoadBalancer: Select server with highest available capacity +LoadBalancer -> Webrtc-Server: Create Room +LoadBalancer -> Redis: Update server capacity +LoadBalancer -> Agent: Return WebSocket URL + +@enduml diff --git a/apps/loadbalancer/docs/room-closure-flow.puml b/apps/loadbalancer/docs/room-closure-flow.puml new file mode 100644 index 0000000..12f19b7 --- /dev/null +++ b/apps/loadbalancer/docs/room-closure-flow.puml @@ -0,0 +1,11 @@ +@startuml +title Room Closure Flow + + + +Agent -> MediasoupServer: Close Room +Webrtc-Server -> LoadBalancer: POST /room-closed +LoadBalancer -> Redis: Add back available capacity +LoadBalancer -> Redis: Remove room entry + +@enduml diff --git a/apps/loadbalancer/docs/server-registration.png b/apps/loadbalancer/docs/server-registration-flow.png similarity index 100% rename from apps/loadbalancer/docs/server-registration.png rename to apps/loadbalancer/docs/server-registration-flow.png diff --git a/apps/loadbalancer/docs/server-registration-flow.puml b/apps/loadbalancer/docs/server-registration-flow.puml new file mode 100644 index 0000000..993a91c --- /dev/null +++ b/apps/loadbalancer/docs/server-registration-flow.puml @@ -0,0 +1,13 @@ +@startuml +title Server Registration Flow + + +entryspacing 1.1 +webrtc-server -> LoadBalancer: POST /register +alt Server already registered + LoadBalancer -> Redis: Remove existing entry +end +LoadBalancer -> LoadBalancer: Calculate capacity (workers × 500) +LoadBalancer -> Redis: Store server data (health: true) + +@enduml From ed58c14578c5c80f0dff974d95677e5fb5193fff Mon Sep 17 00:00:00 2001 From: Gabriel Mata Date: Thu, 6 Feb 2025 17:57:13 -0500 Subject: [PATCH 14/15] ci: --- .github/workflows/cd.yml | 28 ++++++++++++++-------------- .github/workflows/stable-release.yml | 22 +++++++++++----------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 03d7d45..63a566b 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -9,7 +9,7 @@ on: env: DH_USERNAME: ${{secrets.DOCKER_HUB_LOGIN}} DH_TOKEN: ${{secrets.DOCKER_HUB_PWD}} - IMAGE_NAME: 'webrtc-server' + IMAGE_NAME: 'webrtc-server' IMAGE_TAG: ${{ github.ref == 'refs/heads/main' && 'dev' || github.ref }} jobs: @@ -70,19 +70,19 @@ jobs: echo "$DH_TOKEN" | docker login -u "$DH_USERNAME" --password-stdin - name: Build Docker image - if: steps.semantic.outputs.new-release-published == 'true' + if: steps.semantic.outputs.new-release-published == 'true' run: | - docker build -f Dockerfile -t $DH_USERNAME/$IMAGE_NAME:$IMAGE_TAG . - + docker build -f apps/webrtc-server/Dockerfile -t $DH_USERNAME/$IMAGE_NAME:$IMAGE_TAG . + - name: Add tags to Docker image and push to Docker Hub - if: steps.semantic.outputs.new-release-published == 'true' + if: steps.semantic.outputs.new-release-published == 'true' run: | - docker tag $DH_USERNAME/$IMAGE_NAME:$IMAGE_TAG $DH_USERNAME/$IMAGE_NAME:v${RELEASE_MAJOR}-$IMAGE_TAG - docker tag $DH_USERNAME/$IMAGE_NAME:$IMAGE_TAG $DH_USERNAME/$IMAGE_NAME:v${RELEASE_MAJOR}.${RELEASE_MINOR}-$IMAGE_TAG - docker tag $DH_USERNAME/$IMAGE_NAME:$IMAGE_TAG $DH_USERNAME/$IMAGE_NAME:v${RELEASE_MAJOR}.${RELEASE_MINOR}.${RELEASE_PATCH:0:1}-$IMAGE_TAG - docker tag $DH_USERNAME/$IMAGE_NAME:$IMAGE_TAG $DH_USERNAME/$IMAGE_NAME:v$RELEASE_VERSION - docker push $DH_USERNAME/$IMAGE_NAME:$IMAGE_TAG - docker push $DH_USERNAME/$IMAGE_NAME:v$RELEASE_MAJOR-$IMAGE_TAG - docker push $DH_USERNAME/$IMAGE_NAME:v$RELEASE_MAJOR.$RELEASE_MINOR-$IMAGE_TAG - docker push $DH_USERNAME/$IMAGE_NAME:v$RELEASE_MAJOR.$RELEASE_MINOR.${RELEASE_PATCH:0:1}-$IMAGE_TAG - docker push $DH_USERNAME/$IMAGE_NAME:v$RELEASE_VERSION + docker tag $DH_USERNAME/$IMAGE_NAME:$IMAGE_TAG $DH_USERNAME/$IMAGE_NAME:v${RELEASE_MAJOR}-$IMAGE_TAG + docker tag $DH_USERNAME/$IMAGE_NAME:$IMAGE_TAG $DH_USERNAME/$IMAGE_NAME:v${RELEASE_MAJOR}.${RELEASE_MINOR}-$IMAGE_TAG + docker tag $DH_USERNAME/$IMAGE_NAME:$IMAGE_TAG $DH_USERNAME/$IMAGE_NAME:v${RELEASE_MAJOR}.${RELEASE_MINOR}.${RELEASE_PATCH:0:1}-$IMAGE_TAG + docker tag $DH_USERNAME/$IMAGE_NAME:$IMAGE_TAG $DH_USERNAME/$IMAGE_NAME:v$RELEASE_VERSION + docker push $DH_USERNAME/$IMAGE_NAME:$IMAGE_TAG + docker push $DH_USERNAME/$IMAGE_NAME:v$RELEASE_MAJOR-$IMAGE_TAG + docker push $DH_USERNAME/$IMAGE_NAME:v$RELEASE_MAJOR.$RELEASE_MINOR-$IMAGE_TAG + docker push $DH_USERNAME/$IMAGE_NAME:v$RELEASE_MAJOR.$RELEASE_MINOR.${RELEASE_PATCH:0:1}-$IMAGE_TAG + docker push $DH_USERNAME/$IMAGE_NAME:v$RELEASE_VERSION diff --git a/.github/workflows/stable-release.yml b/.github/workflows/stable-release.yml index a06c47e..3177504 100644 --- a/.github/workflows/stable-release.yml +++ b/.github/workflows/stable-release.yml @@ -2,7 +2,7 @@ name: Stable release on: push: tags: - - "*" + - '*' env: DH_USERNAME: ${{secrets.DOCKER_HUB_LOGIN}} DH_TOKEN: ${{secrets.DOCKER_HUB_PWD}} @@ -23,7 +23,7 @@ jobs: - id: get_version uses: battila7/get-version-action@v2 - + - name: Install dependencies run: | npm install @@ -36,15 +36,15 @@ jobs: if: steps.get_version.outputs.is-semver == 'true' run: | docker build -f Dockerfile -t $DH_USERNAME/$IMAGE_NAME:${{steps.get_version.outputs.version}} . - + - name: Add tags to Docker image and push to Docker Hub if: steps.get_version.outputs.is-semver == 'true' run: | - docker tag $DH_USERNAME/$IMAGE_NAME:${{steps.get_version.outputs.version}} $DH_USERNAME/$IMAGE_NAME:v${{steps.get_version.outputs.major}} - docker tag $DH_USERNAME/$IMAGE_NAME:${{steps.get_version.outputs.version}} $DH_USERNAME/$IMAGE_NAME:v${{steps.get_version.outputs.major}}.${{steps.get_version.outputs.minor}} - docker tag $DH_USERNAME/$IMAGE_NAME:${{steps.get_version.outputs.version}} $DH_USERNAME/$IMAGE_NAME:v${{steps.get_version.outputs.major}}.${{steps.get_version.outputs.minor}}.${{steps.get_version.outputs.patch}} - docker tag $DH_USERNAME/$IMAGE_NAME:${{steps.get_version.outputs.version}} $DH_USERNAME/$IMAGE_NAME:v${{steps.get_version.outputs.version-without-v}} - docker push $DH_USERNAME/$IMAGE_NAME:v${{steps.get_version.outputs.major}} - docker push $DH_USERNAME/$IMAGE_NAME:v${{steps.get_version.outputs.major}}.${{steps.get_version.outputs.minor}} - docker push $DH_USERNAME/$IMAGE_NAME:v${{steps.get_version.outputs.major}}.${{steps.get_version.outputs.minor}}.${{steps.get_version.outputs.patch}} - docker push $DH_USERNAME/$IMAGE_NAME:${{steps.get_version.outputs.version}} \ No newline at end of file + docker tag $DH_USERNAME/$IMAGE_NAME:${{steps.get_version.outputs.version}} $DH_USERNAME/$IMAGE_NAME:v${{steps.get_version.outputs.major}} + docker tag $DH_USERNAME/$IMAGE_NAME:${{steps.get_version.outputs.version}} $DH_USERNAME/$IMAGE_NAME:v${{steps.get_version.outputs.major}}.${{steps.get_version.outputs.minor}} + docker tag $DH_USERNAME/$IMAGE_NAME:${{steps.get_version.outputs.version}} $DH_USERNAME/$IMAGE_NAME:v${{steps.get_version.outputs.major}}.${{steps.get_version.outputs.minor}}.${{steps.get_version.outputs.patch}} + docker tag $DH_USERNAME/$IMAGE_NAME:${{steps.get_version.outputs.version}} $DH_USERNAME/$IMAGE_NAME:v${{steps.get_version.outputs.version-without-v}} + docker push $DH_USERNAME/$IMAGE_NAME:v${{steps.get_version.outputs.major}} + docker push $DH_USERNAME/$IMAGE_NAME:v${{steps.get_version.outputs.major}}.${{steps.get_version.outputs.minor}} + docker push $DH_USERNAME/$IMAGE_NAME:v${{steps.get_version.outputs.major}}.${{steps.get_version.outputs.minor}}.${{steps.get_version.outputs.patch}} + docker push $DH_USERNAME/$IMAGE_NAME:${{steps.get_version.outputs.version}} From c8fc43b5bbb4268b8fd131caa2c916905308da81 Mon Sep 17 00:00:00 2001 From: Gabriel Mata Date: Fri, 7 Feb 2025 11:26:06 -0500 Subject: [PATCH 15/15] ci: refactor cd.yml to both apps --- .github/workflows/cd.yml | 47 +++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 63a566b..ee1ef92 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -3,25 +3,22 @@ name: Continuous Deployment on: push: branches: [main, 'release/**'] - workflow_dispatch: env: - DH_USERNAME: ${{secrets.DOCKER_HUB_LOGIN}} - DH_TOKEN: ${{secrets.DOCKER_HUB_PWD}} - IMAGE_NAME: 'webrtc-server' - IMAGE_TAG: ${{ github.ref == 'refs/heads/main' && 'dev' || github.ref }} + DH_USERNAME: ${{ secrets.DOCKER_HUB_LOGIN }} + DH_TOKEN: ${{ secrets.DOCKER_HUB_PWD }} jobs: build-and-push: - name: Build and push docker images + name: Build and push Docker images runs-on: ubuntu-latest steps: - - name: Checkout webrtc-server + - name: Checkout repository uses: actions/checkout@v4 - - name: Setup node v20 + - name: Setup Node.js v20 uses: actions/setup-node@v4 with: node-version: 20 @@ -30,7 +27,8 @@ jobs: run: | npm install - - uses: codfish/semantic-release-action@v3 + - name: Run semantic release + uses: codfish/semantic-release-action@v3 id: semantic with: branches: | @@ -61,7 +59,7 @@ jobs: "npmPublish": false } ], - "@semantic-release/release-notes-generator", + "@semantic-release/release-notes-generator" ] - name: Log in to Docker Hub @@ -69,20 +67,25 @@ jobs: run: | echo "$DH_TOKEN" | docker login -u "$DH_USERNAME" --password-stdin - - name: Build Docker image + - name: Build Docker images if: steps.semantic.outputs.new-release-published == 'true' run: | - docker build -f apps/webrtc-server/Dockerfile -t $DH_USERNAME/$IMAGE_NAME:$IMAGE_TAG . + for APP in webrtc-server loadbalancer; do + docker build -f apps/$APP/Dockerfile -t $DH_USERNAME/$APP:$IMAGE_TAG . + done - - name: Add tags to Docker image and push to Docker Hub + - name: Add tags to Docker images and push to Docker Hub if: steps.semantic.outputs.new-release-published == 'true' run: | - docker tag $DH_USERNAME/$IMAGE_NAME:$IMAGE_TAG $DH_USERNAME/$IMAGE_NAME:v${RELEASE_MAJOR}-$IMAGE_TAG - docker tag $DH_USERNAME/$IMAGE_NAME:$IMAGE_TAG $DH_USERNAME/$IMAGE_NAME:v${RELEASE_MAJOR}.${RELEASE_MINOR}-$IMAGE_TAG - docker tag $DH_USERNAME/$IMAGE_NAME:$IMAGE_TAG $DH_USERNAME/$IMAGE_NAME:v${RELEASE_MAJOR}.${RELEASE_MINOR}.${RELEASE_PATCH:0:1}-$IMAGE_TAG - docker tag $DH_USERNAME/$IMAGE_NAME:$IMAGE_TAG $DH_USERNAME/$IMAGE_NAME:v$RELEASE_VERSION - docker push $DH_USERNAME/$IMAGE_NAME:$IMAGE_TAG - docker push $DH_USERNAME/$IMAGE_NAME:v$RELEASE_MAJOR-$IMAGE_TAG - docker push $DH_USERNAME/$IMAGE_NAME:v$RELEASE_MAJOR.$RELEASE_MINOR-$IMAGE_TAG - docker push $DH_USERNAME/$IMAGE_NAME:v$RELEASE_MAJOR.$RELEASE_MINOR.${RELEASE_PATCH:0:1}-$IMAGE_TAG - docker push $DH_USERNAME/$IMAGE_NAME:v$RELEASE_VERSION + for APP in webrtc-server loadbalancer; do + docker tag $DH_USERNAME/$APP:$IMAGE_TAG $DH_USERNAME/$APP:v${RELEASE_MAJOR}-$IMAGE_TAG + docker tag $DH_USERNAME/$APP:$IMAGE_TAG $DH_USERNAME/$APP:v${RELEASE_MAJOR}.${RELEASE_MINOR}-$IMAGE_TAG + docker tag $DH_USERNAME/$APP:$IMAGE_TAG $DH_USERNAME/$APP:v${RELEASE_MAJOR}.${RELEASE_MINOR}.${RELEASE_PATCH:0:1}-$IMAGE_TAG + docker tag $DH_USERNAME/$APP:$IMAGE_TAG $DH_USERNAME/$APP:v$RELEASE_VERSION + + docker push $DH_USERNAME/$APP:$IMAGE_TAG + docker push $DH_USERNAME/$APP:v$RELEASE_MAJOR-$IMAGE_TAG + docker push $DH_USERNAME/$APP:v$RELEASE_MAJOR.$RELEASE_MINOR-$IMAGE_TAG + docker push $DH_USERNAME/$APP:v$RELEASE_MAJOR.$RELEASE_MINOR.${RELEASE_PATCH:0:1}-$IMAGE_TAG + docker push $DH_USERNAME/$APP:v$RELEASE_VERSION + done