commit f1dd557c5d7373715bedf11a4649098a70567632 Author: CREATIVE_tg1 Date: Sun Jun 21 03:53:06 2026 +0300 init diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6f5a6f8 --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +# See config.ts for list of server config variables +# See src/config.ts for list of client config variables + +# Leave blank to allow access to all features without login +VITE_FIREBASE_CONFIG= +# VITE_FIREBASE_CONFIG={"apiKey":"AIzaSyA2fkXeFokJ-Ei_jnzDso5AmjbIaMdzuEc","authDomain":"watchparty-273604.firebaseapp.com","databaseURL":"https://watchparty-273604.firebaseio.com","projectId":"watchparty-273604","storageBucket":"watchparty-273604.appspot.com","messagingSenderId":"769614672795","appId":"1:769614672795:web:54bbda86288ab1a034273e"} + +# All other config is optional (see README) +# YOUTUBE_API_KEY=REPLACE_ME +# FIREBASE_ADMIN_SDK_CONFIG=REPLACE_ME +# DATABASE_URL=postgresql://postgres@localhost:5432/postgres?sslmode=disable +# REDIS_URL=localhost:6379 \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1bf21e6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml new file mode 100644 index 0000000..468e00f --- /dev/null +++ b/.github/workflows/nodejs.yml @@ -0,0 +1,36 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: Node.js CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js + uses: actions/setup-node@v1 + with: + node-version: 24.x + - run: npm install + - run: npm run build --if-present + env: + CI: true + NODE_ENV: production + - name: Push to remote + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "watchparty+github-actions[bot]@users.noreply.github.com" + rm .gitignore + git checkout -b release + git add --all + git commit --quiet -m "Built from ${{ github.sha }}" + git push origin release -f diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ffbc428 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +.theia +.eslintcache \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..25fa621 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2466b30 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +## April 2023 + +- Added support for looping the same video +- Player now shows the buffered/downloaded ranges of video available +- Added a 3x speed playback option +- HLS/m3u8 live vs. non-live streams are now handled properly +- Improved HLS playback experience on Safari and Android Chrome +- Pasting Reddit links with video media is now supported +- Twitch links (streams and VODs) are now supported diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f1baf08 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM node:24-alpine + +COPY . /usr/src + +WORKDIR /usr/src + +RUN npm install + +RUN npm run build + +ENTRYPOINT ["/bin/sh", "-c" , "npm start"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a84a357 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Howard Chung + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..59383e2 --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# WatchParty + +![screenshot](https://github.com/howardchung/watchparty/raw/master/public/screenshot_full.png) + +A website for watching videos together. + +## Description + +- Synchronizes the video being watched with the current room +- Plays, pauses, and seeks are synced to all watchers +- Supports: + - Screen sharing (full screen, browser tab or application) + - Launch a shared virtual browser in the cloud (similar to rabb.it) + - Stream-your-own-file + - Video files on the Internet (anything accessible via HTTP) + - YouTube videos + - Magnet links (via WebTorrent) + - .m3u8 streams (HLS) +- Create separate rooms for users on demand +- Text chat +- Video chat + +## Quick Start + +- Clone this repo via `git clone git@github.com:howardchung/watchparty.git` +- Install npm dependencies for the project via `npm install` +- Start the server via `npm run dev` + - Defaults to port 8080, customize with `PORT` env var + - Set `SSL_KEY_FILE` and `SSL_CRT_FILE` for HTTPS. +- Start the React application in a separate shell and port via `npm run ui` + - Point to server using `VITE_SERVER_HOST` env var if you customized it above + - Set `SSL_KEY_FILE` and `SSL_CRT_FILE` for HTTPS. + - HTTPS is required by the browser for some WebRTC features (camera, etc.) +- Duplicate the `.env.example` file +- Rename it to `.env` +- Add config for the features you want as described in the advanced setup + +## Advanced Setup (optional) + +All of these are optional and the application should work without them. Some functionality may be missing. + +### YouTube API (video search) + +This project uses the YouTube API for video search, which requires an API key. You can get one from Google [here](https://console.developers.google.com). + +Without an API key you won't be able to search for videos via the searchbox. + +After creating a **YouTube Data API V3** access, you can create an API key which you can add to your environment variables by copying the `.env.example`, renaming it to `.env` and adding the key to the YOUTUBE_API_KEY variable. + +After that restart your server to enable the YouTube API access on your server. + +### Firebase Config (user authentication) + +This project uses Firebase for authentication. This is used for user login, account management, subscriptions, and handling some features like room locking/permanence. + +To set up, create a new Firebase app (or reuse an old one) from [here](https://console.firebase.google.com/). After creating an application, click on the settings cog icon in the left menu next to "Project overview" and click on project settings. From there, scroll down, create a web application and copy the Firebase SDK configuration snippet JSON data. + +Next, you have to stringify it: `JSON.stringify(PASTE_CONFIG_HERE)` in your browser console, then add it to `VITE_FIREBASE_CONFIG` in your .env file. + +For server verification of accounts you'll also need `FIREBASE_ADMIN_SDK_CONFIG`, which you should do the same steps for. + +### Virtual Browser Setup + +This project supports creating virtual browsers (using https://github.com/m1k1o/neko) either on a cloud provider or with Docker containers. For development, Docker is easiest. + +- Install Docker: `curl -fsSL https://get.docker.com | sh` +- Make sure you have an SSH key pair set up on the server (`id_rsa` in `~/.ssh` directory), if not, use `ssh-keygen`. +- Configure `DOCKER_VM_HOST_SSH_USER` if `root` is not the correct user +- Note: If your web client is not running on the same physical machine as the server, you will also need to configure `DOCKER_VM_HOST` to a publically-resolvable value (i.e. not localhost) +- If you want to run managed instance pools (whether on cloud or with Docker), configure `VM_MANAGER_CONFIG` and run the vmWorker service. + +### Room Persistence + +- Configure Postgres by adding DATABASE_URL to your .env file and then setting up the database schema +- This allows rooms to persist between server restarts + +## Tech + +- React +- TypeScript +- Node.js +- Redis +- PostgreSQL +- Docker diff --git a/dev/audio.html b/dev/audio.html new file mode 100644 index 0000000..3089a7b --- /dev/null +++ b/dev/audio.html @@ -0,0 +1,87 @@ + + + + diff --git a/dev/backup.sh b/dev/backup.sh new file mode 100644 index 0000000..a6745c2 --- /dev/null +++ b/dev/backup.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +sudo docker exec -it --user postgres postgres sh -c "pg_dump postgres" > postgres.bak + +sudo docker exec -it redis sh -c "cat dump.rdb" > dump.rdb + +# on local: scp root@ip:dump.rdb dump.rdb + +sudo docker exec -it --user postgres postgres sh -c "psql postgres" < postgres.bak + +sudo docker exec -it redis sh -c "cat > dump.rdb" < dump.rdb \ No newline at end of file diff --git a/dev/browseparty/chrome/Dockerfile b/dev/browseparty/chrome/Dockerfile new file mode 100644 index 0000000..3a0fd8f --- /dev/null +++ b/dev/browseparty/chrome/Dockerfile @@ -0,0 +1,7 @@ +FROM siomiz/chrome + +RUN apt-get update + +RUN apt-get install -y libasound2 alsa-utils alsa-oss + +RUN apt-get install -y pulseaudio \ No newline at end of file diff --git a/dev/browseparty/dev/Dockerfile b/dev/browseparty/dev/Dockerfile new file mode 100644 index 0000000..79a3603 --- /dev/null +++ b/dev/browseparty/dev/Dockerfile @@ -0,0 +1,35 @@ +FROM ubuntu:xenial + +RUN apt-get update; apt-get clean + +# Add a user for running applications. +RUN useradd apps +RUN mkdir -p /home/apps && chown apps:apps /home/apps + +# Install x11vnc. +RUN apt-get install -y x11vnc + +# Install xvfb. +RUN apt-get install -y xvfb + +# Install fluxbox. +RUN apt-get install -y fluxbox + +# Install wget. +RUN apt-get install -y wget + +# Install wmctrl. +RUN apt-get install -y wmctrl + +RUN apt-get install -y gnupg2 + +# Set the Chrome repo. +RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ + && echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list + +# Install Chrome. +RUN apt-get update && apt-get -y install google-chrome-stable + +COPY bootstrap.sh / + +CMD '/bootstrap.sh' \ No newline at end of file diff --git a/dev/browseparty/dev/bootstrap.sh b/dev/browseparty/dev/bootstrap.sh new file mode 100644 index 0000000..468d349 --- /dev/null +++ b/dev/browseparty/dev/bootstrap.sh @@ -0,0 +1,88 @@ +#!/bin/bash + +# Based on: http://www.richud.com/wiki/Ubuntu_Fluxbox_GUI_with_x11vnc_and_Xvfb + +readonly G_LOG_I='[INFO]' +readonly G_LOG_W='[WARN]' +readonly G_LOG_E='[ERROR]' + +main() { + launch_xvfb + launch_window_manager + run_vnc_server +} + +launch_xvfb() { + # Set defaults if the user did not specify envs. + export DISPLAY=${XVFB_DISPLAY:-:1} + local screen=${XVFB_SCREEN:-0} + local resolution=${XVFB_RESOLUTION:-1280x1024x24} + local timeout=${XVFB_TIMEOUT:-5} + + # Start and wait for either Xvfb to be fully up or we hit the timeout. + Xvfb ${DISPLAY} -screen ${screen} ${resolution} & + local loopCount=0 + until xdpyinfo -display ${DISPLAY} > /dev/null 2>&1 + do + loopCount=$((loopCount+1)) + sleep 1 + if [ ${loopCount} -gt ${timeout} ] + then + echo "${G_LOG_E} xvfb failed to start." + exit 1 + fi + done +} + +launch_window_manager() { + local timeout=${XVFB_TIMEOUT:-5} + + # Start and wait for either fluxbox to be fully up or we hit the timeout. + fluxbox & + local loopCount=0 + until wmctrl -m > /dev/null 2>&1 + do + loopCount=$((loopCount+1)) + sleep 1 + if [ ${loopCount} -gt ${timeout} ] + then + echo "${G_LOG_E} fluxbox failed to start." + exit 1 + fi + done +} + +run_vnc_server() { + local passwordArgument='-nopw' + + if [ -n "${VNC_SERVER_PASSWORD}" ] + then + local passwordFilePath="${HOME}/x11vnc.pass" + if ! x11vnc -storepasswd "${VNC_SERVER_PASSWORD}" "${passwordFilePath}" + then + echo "${G_LOG_E} Failed to store x11vnc password." + exit 1 + fi + passwordArgument=-"-rfbauth ${passwordFilePath}" + echo "${G_LOG_I} The VNC server will ask for a password." + else + echo "${G_LOG_W} The VNC server will NOT ask for a password." + fi + + x11vnc -display ${DISPLAY} -forever ${passwordArgument} & + wait $! +} + +control_c() { + echo "" + exit +} + +#trap control_c SIGINT SIGTERM SIGHUP + +main + +sleep 1 +google-chrome --start-maximized --no-first-run --display ${DISPLAY} + +exit \ No newline at end of file diff --git a/dev/browseparty/httpforwarder.ts b/dev/browseparty/httpforwarder.ts new file mode 100644 index 0000000..56fade9 --- /dev/null +++ b/dev/browseparty/httpforwarder.ts @@ -0,0 +1,16 @@ +import http from "node:http"; +import net from "node:net"; + +const port = 5902; + +const client = new net.Socket(); +const server = http.createServer((req, res) => { + console.log(req.url); + res.setHeader("Content-Type", "audio/ogg"); + client.connect(5901, "localhost"); + client.pipe(res); +}); + +server.listen(port, () => { + console.log(`server is listening on ${port}`); +}); diff --git a/dev/browseparty/init.sh b/dev/browseparty/init.sh new file mode 100644 index 0000000..5f94c34 --- /dev/null +++ b/dev/browseparty/init.sh @@ -0,0 +1,29 @@ +# Enable kernel modules on the host +modprobe snd-aloop + +# Install gstreamer +apt-get install -y libgstreamer1.0-0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-doc gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa gstreamer1.0-gl gstreamer1.0-gtk3 gstreamer1.0-qt5 gstreamer1.0-pulseaudio + +# Inside container: ALSA to view sound devices with aplay -l +apt-get install -y libasound2 alsa-utils alsa-oss +apt-get install -y pulseaudio + +# Rebuild our container +docker build . -t browseparty/chrome + +# Run our services +# Start chrome + VNC, sharing audio device +docker run -d -p 5900:5900 --rm --name chrome --device /dev/snd -e VNC_PASSWORD="password" -e VNC_SCREEN_SIZE="1280x720" -e CHROME_OPTS_OVERRIDE=" --user-data-dir --no-sandbox --no-first-run --window-position=0,0 --force-device-scale-factor=1 --disable-dev-shm-usage" browseparty/chrome + +# Proxy the VNC from TCP to WebSocket +docker run -d -it --rm --net=host --name websockify efrecon/websockify 5000 localhost:5900 + +# Audio test +speaker-test -D hw:0,0,0 --channels 2 --rate 48000 + +# Stream the audio to a TCP sink +gst-launch-1.0 -v alsasrc device=hw:0,1,0 ! audio/x-raw, channels=2, rate=48000 ! opusenc complexity=0 ! oggmux ! tcpserversink port=5901 host=0.0.0.0 +gst-launch-1.0 -v alsasrc device=hw:0,1,0 ! audio/x-raw, channels=2, rate=48000 ! audioconvert ! lamemp3enc ! tcpserversink port=5901 host=0.0.0.0 + +# Proxy the audio from TCP to WebSocket +docker run -d -it --rm --net=host --name websockify-audio efrecon/websockify 5001 localhost:5901 diff --git a/dev/cors.json b/dev/cors.json new file mode 100644 index 0000000..1ca2537 --- /dev/null +++ b/dev/cors.json @@ -0,0 +1,11 @@ +{ + "CORSRules": [ + { + "AllowedOrigins": ["*"], + "AllowedHeaders": ["*"], + "AllowedMethods": ["GET", "HEAD", "POST", "PUT", "DELETE"], + "MaxAgeSeconds": 3000, + "ExposeHeaders": ["Etag"] + } + ] +} diff --git a/dev/cors.xml b/dev/cors.xml new file mode 100644 index 0000000..fd21969 --- /dev/null +++ b/dev/cors.xml @@ -0,0 +1,9 @@ + + + * + GET + * + 3000 + ETag + + \ No newline at end of file diff --git a/dev/coturn.sh b/dev/coturn.sh new file mode 100644 index 0000000..ca2b5c9 --- /dev/null +++ b/dev/coturn.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# install docker +curl -fsSL https://get.docker.com | sh + +# coturn +docker run -d --network=host --name=coturn coturn/coturn \ + -n --log-file=stdout \ + --lt-cred-mech --fingerprint \ + --no-multicast-peers --no-cli \ + --no-tlsv1 --no-tlsv1_1 \ + --realm=watchparty \ +# doesn't actually work, manually docker exec bash and run the add +docker exec -it --user nobody coturn "bash -c 'turnadmin --add -u username -p password -r watchparty'" + diff --git a/dev/dockerbox.sh b/dev/dockerbox.sh new file mode 100644 index 0000000..720bfe4 --- /dev/null +++ b/dev/dockerbox.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +apt-get update +apt-get install -y curl + +# install docker +curl -fsSL https://get.docker.com | sh +# pull vbrowser image +docker pull howardc93/vbrowser +# install certbot +DEBIAN_FRONTEND=noninteractive apt-get install -y certbot +certbot certonly --standalone -n --email howardzchung@gmail.com --agree-tos -d docker1.watchparty.me +chmod -R 755 /etc/letsencrypt/live/ +chmod -R 755 /etc/letsencrypt/archive/ diff --git a/dev/gateway.sh b/dev/gateway.sh new file mode 100644 index 0000000..35b09d3 --- /dev/null +++ b/dev/gateway.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +apt update + +DEBIAN_FRONTEND=noninteractive apt-get install -y certbot python3-certbot-dns-cloudflare +certbot certonly --standalone -n --email howardzchung@gmail.com --agree-tos -d $(hostname).watchparty.me +#certbot certonly --dns-cloudflare --dns-cloudflare-credentials ~/cloudflare.ini -d *.watchparty.me --preferred-challenges dns-01 + +apt install -y nginx +apt install -y bind9 + +echo 'events {} +http { + server { + listen 443 ssl; + server_name HOSTNAME_PLACEHOLDER.watchparty.me; + ssl_certificate /etc/letsencrypt/live/HOSTNAME_PLACEHOLDER.watchparty.me/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/HOSTNAME_PLACEHOLDER.watchparty.me/privkey.pem; + + location / { + resolver 127.0.0.1; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_pass http://$arg_ip; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + } +}' | sed "s/HOSTNAME_PLACEHOLDER/$(hostname)/g" > /etc/nginx/nginx.conf +/etc/init.d/nginx reload + +# Update /etc/letsencrypt/cli.ini with renew hook +echo ' +deploy-hook = systemctl reload nginx' >> /etc/letsencrypt/cli.ini \ No newline at end of file diff --git a/dev/ioredisblpoptest.ts b/dev/ioredisblpoptest.ts new file mode 100644 index 0000000..611b9a6 --- /dev/null +++ b/dev/ioredisblpoptest.ts @@ -0,0 +1,8 @@ +import { Redis } from "ioredis"; + +const redis = new Redis("localhost:6379"); +const redis2 = new Redis("localhost:6379"); +redis.blpop("ioRedisTest", 0); +redis.disconnect(); +await redis2.lpush("ioRedisTest", "val"); +console.log(await redis2.lrange("ioRedisTest", 0, -1)); diff --git a/dev/redis.sh b/dev/redis.sh new file mode 100644 index 0000000..ae26906 --- /dev/null +++ b/dev/redis.sh @@ -0,0 +1,2 @@ +#!/bin/bash +docker run --name watchparty-redis --rm --net=host -d redis \ No newline at end of file diff --git a/dev/renamer.ts b/dev/renamer.ts new file mode 100644 index 0000000..a9bfb2b --- /dev/null +++ b/dev/renamer.ts @@ -0,0 +1,21 @@ +import fs from "node:fs"; +import xml2js from "xml2js"; + +const xml = ``; + +const subPath = "/var/www/html/subtitles/"; +const subs = fs.readdirSync(subPath); +let vids = fs.readdirSync("/var/www/html/"); +const data = await xml2js.parseStringPromise(xml); +// vids = [...data.ListBucketResult.Contents.filter(file => file.Key[0].startsWith('')).map(file => file.Key[0])]; +console.log(subs); +console.log(vids); +vids.forEach((vid) => { + const match = subs.find((sub) => + vid.toLowerCase().startsWith(sub.slice(0, -4).toLowerCase()), + ); + console.log(vid, match); + if (vid && match) { + fs.copyFileSync(subPath + match, subPath + vid + ".srt"); + } +}); diff --git a/dev/slashCommand.py b/dev/slashCommand.py new file mode 100644 index 0000000..8cbadf1 --- /dev/null +++ b/dev/slashCommand.py @@ -0,0 +1,29 @@ +import requests + +url = "https://discord.com/api/v10/applications/1071394728513380372/commands" + +# This is an example CHAT_INPUT or Slash Command, with a type of 1 +json = { + "name": "watch", + "type": 1, + "description": "Start a new WatchParty", + "options": [ + { + "name": "video", + "description": "The URL to start watching", + "type": 3 + } + ] +} + +# For authorization, you can use either your bot token +headers = { + "Authorization": "Bot " +} + +# or a client credentials token for your app with the applications.commands.update scope +# headers = { +# "Authorization": "Bearer " +# } + +r = requests.post(url, headers=headers, json=json) \ No newline at end of file diff --git a/dev/test.html b/dev/test.html new file mode 100644 index 0000000..db86f8d --- /dev/null +++ b/dev/test.html @@ -0,0 +1,96 @@ + + + diff --git a/dev/testmakename.ts b/dev/testmakename.ts new file mode 100644 index 0000000..438472a --- /dev/null +++ b/dev/testmakename.ts @@ -0,0 +1,12 @@ +import { resolveShard } from "../server/utils/resolveShard"; +import { makeName } from "../server/utils/moniker"; + +for (let i = 0; i < 100; i++) { + const shard = (i % 2) + 1; + const name = makeName(shard); + const nameShard = resolveShard(name); + console.log(name, shard, nameShard); + if (shard !== nameShard) { + throw new Error("shard mismatch"); + } +} diff --git a/dev/uploader.ts b/dev/uploader.ts new file mode 100644 index 0000000..df67652 --- /dev/null +++ b/dev/uploader.ts @@ -0,0 +1,51 @@ +import axios from "axios"; +import fs from "node:fs"; +import s3 from "s3-client"; + +var client = s3.createClient({ + maxAsyncS3: 20, // this is the default + s3RetryCount: 3, // this is the default + s3RetryDelay: 1000, // this is the default + multipartUploadThreshold: 2097152000, + multipartUploadSize: 15728640, // this is the default (15 MB) + s3Options: { + accessKeyId: process.env.SCW_ACCESS_KEY, + secretAccessKey: process.env.SCW_SECRET_KEY, + endpoint: "s3.fr-par.scw.cloud", + // endpoint: 's3.yourdomain.com', + // sslEnabled: false + // any other options are passed to new AWS.S3() + // See: http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Config.html#constructor-property + }, +}); + +var params = { + localDir: "/var/www/html", + deleteRemoved: false, // default false, whether to remove s3 objects that have no corresponding local file. + s3Params: { + Bucket: "hcmedia", + Prefix: "", + }, + getS3Params: (localFile, stat, callback) => { + // call callback like this: + var s3Params = { + // if there is no error + Key: localFile.replace(/ /g, "."), + ACL: "public-read", + // other options supported by putObject, except Body and ContentLength. + // See: http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#putObject-property + }; + // pass `null` for `s3Params` if you want to skip uploading this file. + callback(null, s3Params); + }, +}; +var uploader = client.uploadDir(params); +uploader.on("error", function (err) { + console.error("unable to sync:", err.stack); +}); +uploader.on("progress", function () { + console.log("progress", uploader.progressAmount, uploader.progressTotal); +}); +uploader.on("end", function () { + console.log("done uploading"); +}); diff --git a/dev/vbrowser.sh b/dev/vbrowser.sh new file mode 100644 index 0000000..f919aad --- /dev/null +++ b/dev/vbrowser.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# apt-get update +# apt-get install -y curl + +# install docker (if not already installed) +# curl -fsSL https://get.docker.com | sh + +# install dnsutils (iptables?) +apt-get install -y dnsutils + +# disable unattended-upgrades +# apt-get remove -y unattended-upgrades + +# determine if this is a large or not via nproc +# This systemd config starts the vbrowser on reboot (or instances created from a snapshot of this init vm) +echo ' +#!/bin/bash + +iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 5000 +PASSWORD=$(uuidgen) +#RESOLUTION=$(if [ "$(nproc)" -le "2" ]; then echo "1280x720@30"; else echo "1920x1080@30"; fi) +RESOLUTION="1280x720@30" +ADMIN_KEY={VBROWSER_ADMIN_KEY} +# docker pull howardc93/vbrowser +docker run -d --rm --name=vbrowser --log-opt max-size=1g --net=host --shm-size=1g --cap-add="SYS_ADMIN" -e DISPLAY=":99.0" -e NEKO_SCREEN=$RESOLUTION -e NEKO_PASSWORD=$PASSWORD -e NEKO_PASSWORD_ADMIN=$PASSWORD -e NEKO_ADMIN_KEY=$ADMIN_KEY -e NEKO_BIND=":5000" -e NEKO_EPR=":59000-59100" -e NEKO_H264="1" howardc93/vbrowser +' > /etc/systemd/system/vbrowser.sh +chmod u+x /etc/systemd/system/vbrowser.sh + +echo '[Unit] +Description=Start VBrowser + +[Service] +Type=oneshot +ExecStart=bash /etc/systemd/system/vbrowser.sh + +[Install] +WantedBy=multi-user.target' > /etc/systemd/system/vbrowser.service +systemctl enable vbrowser.service + +# This ensures the image is pre-pulled and also creates an endpoint for updateSnapshot to check +docker run -d --rm --name=test --net=host --shm-size=1g --cap-add="SYS_ADMIN" -e DISPLAY=":99.0" -e NEKO_PASSWORD=neko -e NEKO_PASSWORD_ADMIN=admin -e NEKO_BIND=":5000" -e NEKO_EPR=":59000-59100" howardc93/vbrowser diff --git a/dev/watchparty.sh b/dev/watchparty.sh new file mode 100644 index 0000000..1f6b5c3 --- /dev/null +++ b/dev/watchparty.sh @@ -0,0 +1,98 @@ +# Allow ssh, http, https +ufw allow http +ufw allow https +ufw allow ssh +ufw allow 3000 +# Enable ufw +ufw enable + +# Install nginx/bind9 +apt install -y nginx +apt install -y bind9 + +echo 'worker_rlimit_nofile 4096; +events { + worker_connections 4096; +} +http { + + upstream rr { + server 127.0.0.1:3001; + server 127.0.0.1:3002; + # server 127.0.0.1:3003; + } + + upstream 1 { + server 127.0.0.1:3001; + } + + upstream 2 { + server 127.0.0.1:3002; + } + + upstream 3 { + server 127.0.0.1:3003; + } + + map $arg_shard $pool { + default "rr"; + 1 "1"; + 2 "2"; + # 3 "3"; + } + + server { + listen 80; + location / { + proxy_pass http://$pool; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $host; + + # enable WebSockets + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + } +}' > /etc/nginx/nginx.conf +/etc/init.d/nginx reload + +# Install git +apt update +apt install -y git + +# Clone application code +git clone https://github.com/howardchung/watchparty + +# Install docker +curl -sSL https://get.docker.com/ | sh +# Start Redis +sudo docker run --log-opt max-size=1g -d --name redis --restart=always --net=host redis +# Start Postgres +sudo docker run --log-opt max-size=1g -d --name postgres --restart=always -e POSTGRES_PASSWORD=password --net=host -v $PWD/sql/:/docker-entrypoint-initdb.d/ postgres + +# Install NodeJS +curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - +sudo apt-get install -y nodejs + +# Set up certbot or Cloudflare HTTPS + +# Build watchparty +npm run build + +# Set .env config +echo ' +DATABASE_URL=postgresql://postgres@localhost:5432/postgres?sslmode=disable +REDIS_URL=localhost:6379 +' > .env + +# Install PM2 globally +npm install -g pm2 + +# PM2 start +npm run pm2 + +# Run on startup +./node_modules/pm2/bin/pm2 startup +./node_modules/pm2/bin/pm2 save \ No newline at end of file diff --git a/global.d.ts b/global.d.ts new file mode 100644 index 0000000..3f1b260 --- /dev/null +++ b/global.d.ts @@ -0,0 +1,106 @@ +declare module "srt-webvtt"; + +type StringDict = Record; +type NumberDict = Record; +type BooleanDict = Record; +type AnyDict = Record; +type PCDict = Record; +type HTMLVideoElementDict = Record; +type MediaType = "vbrowser" | "screenshare" | "video" | "youtube"; + +interface User { + id: string; + isVideoChat?: boolean; + isMuted?: boolean; + isScreenShare?: boolean; +} + +interface Reaction { + user: string; + value: string; + msgId: string; + msgTimestamp: string; +} + +type ChatPayload = { + msg: string; + replyToId?: string; + replyToTimestamp?: string; +}; + +interface ChatMessageBase { + id: string; + cmd?: string; + msg?: string; + system?: boolean; + isSub?: boolean; + replyToId?: string; + replyToTimestamp?: string; + replyToUserId?: string; + replyToMsg?: string; +} + +interface ChatMessage extends ChatMessageBase { + timestamp: string; + videoTS?: number; + reactions?: { [value: string]: string[] }; +} + +interface Settings { + disableChatSound?: boolean; +} + +interface PlaylistVideo { + url: string; + name: string; + img?: string; + channel?: string; + duration: number; + type: string; +} + +interface SearchResult extends PlaylistVideo { + size?: string | number; + seeders?: string; + magnet?: string; + type: "youtube" | "file" | "magnet"; + url: string; + name: string; + duration: number; +} + +interface HostState { + video: string; + videoTS: number; + subtitle: string; + paused: boolean; + isVBrowserLarge: boolean; + controller?: string; + playbackRate: number; + loop: boolean; +} + +interface PersistentRoom { + roomId: string; + password: string; + owner: string; + vanity: string; + isChatDisabled: boolean; + isSubRoom: boolean; + data: any; +} + +interface LinkAccount { + accountname: string; + accountid: string; + discriminator: string; + kind: string; +} + +interface ShardMetric { + uptime: number; + mem: number; + roomCount: number; + users: number; + vbWaiting: number; +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..0c6cb91 --- /dev/null +++ b/index.html @@ -0,0 +1,64 @@ + + + + + + + + + + + WatchParty + + + + + +
+ + + + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9161d82 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,11820 @@ +{ + "name": "watchparty", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "watchparty", + "version": "0.1.0", + "dependencies": { + "@googleapis/youtube": "^8.0.2", + "axios": "^1.13.2", + "body-parser": "^1.20.4", + "compression": "^1.8.1", + "cors": "^2.8.5", + "discord.js": "^14.25.1", + "express": "^5.2.1", + "firebase-admin": "^11.11.1", + "ioredis": "^5.8.2", + "node-ssh": "^13.2.1", + "pg": "^8.16.3", + "pm2": "^6.0.14", + "socket.io": "^4.8.1", + "stripe": "^8.222.0" + }, + "devDependencies": { + "@emoji-mart/data": "^1.2.1", + "@emoji-mart/react": "^1.1.1", + "@mantine/core": "^8.3.10", + "@stripe/stripe-js": "^1.54.2", + "@tabler/icons-react": "^3.35.0", + "@types/compression": "^1.8.1", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/node": "^25.0.1", + "@types/pg": "^8.16.0", + "@types/react": "^18.3.27", + "@types/react-dom": "^18.3.7", + "@types/react-onclickoutside": "^6.7.10", + "@types/react-router-dom": "^5.3.3", + "@types/react-transition-group": "^4.4.12", + "@types/webtorrent": "^0.110.1", + "@types/youtube": "^0.1.2", + "can-autoplay": "^3.0.2", + "dashjs": "^5.1.0", + "eventemitter3": "^4.0.7", + "firebase": "^9.23.0", + "hls.js": "^1.6.15", + "mediasoup-client": "^3.18.1", + "react": "^18.3.1", + "react-colorful": "^5.6.1", + "react-countup": "^6.5.3", + "react-dom": "^18.3.1", + "react-linkify": "^1.0.0-alpha", + "react-onclickoutside": "^6.13.2", + "react-router-dom": "^5.3.4", + "react-secure-link": "^3.2.0", + "react-transition-group": "^4.4.5", + "recharts": "^2.15.4", + "socket.io-client": "^4.8.1", + "srt-webvtt": "^2.0.0", + "twitch-m3u8": "^1.1.5", + "typescript": "^5.9.3", + "vite": "^7.2.7", + "webtorrent": "^2.8.5" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", + "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", + "optional": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@discordjs/builders": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.0.tgz", + "integrity": "sha512-COK0uU6ZaJI+LA67H/rp8IbEkYwlZf3mAoBI5wtPh5G5cbEQGNhVpzINg2f/6+q/YipnNIKy6fJDg6kMUKUw4Q==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/formatters": "^0.6.1", + "@discordjs/util": "^1.1.1", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.38.31", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/collection": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/formatters": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz", + "integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.33" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz", + "integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.1.1", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.38.16", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/util": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz", + "integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.33" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz", + "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.5.1", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "^0.38.1", + "tslib": "^2.6.2", + "ws": "^8.17.0" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@emoji-mart/data": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emoji-mart/data/-/data-1.2.1.tgz", + "integrity": "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emoji-mart/react": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@emoji-mart/react/-/react-1.1.1.tgz", + "integrity": "sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==", + "dev": true, + "peerDependencies": { + "emoji-mart": "^5.2", + "react": "^16.8 || ^17 || ^18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/busboy": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.2.1.tgz", + "integrity": "sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q==", + "dependencies": { + "text-decoding": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.0.tgz", + "integrity": "sha512-Locv8gAqx0e+GX/0SI3dzmBY5e9kjVDtD+3zCFLJ0tH2hJwuCAiL+5WkHuxKj92rqQj/rvkBUCfA1ewlX2hehg==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.6.tgz", + "integrity": "sha512-4MqpVLFkGK7NJf/5wPEEP7ePBJatwYpyjgJ+wQHQGHfzaCDgntOnl9rL2vbVGGKCnRqWtZDIWhctB86UWXaX2Q==", + "dev": true, + "dependencies": { + "@firebase/analytics": "0.10.0", + "@firebase/analytics-types": "0.8.0", + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.0.tgz", + "integrity": "sha512-iRP+QKI2+oz3UAh4nPEq14CsEjrjD6a5+fuypjScisAh9kXKFvdJOZJDwk7kikLvWVLGEs9+kIUS4LPQV7VZVw==", + "dev": true + }, + "node_modules/@firebase/app": { + "version": "0.9.13", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.9.13.tgz", + "integrity": "sha512-GfiI1JxJ7ecluEmDjPzseRXk/PX31hS7+tjgBopL7XjB2hLUdR+0FTMXy2Q3/hXezypDvU6or7gVFizDESrkXw==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.8.0.tgz", + "integrity": "sha512-dRDnhkcaC2FspMiRK/Vbp+PfsOAEP6ZElGm9iGFJ9fDqHoPs0HOPn7dwpJ51lCFi1+2/7n5pRPGhqF/F03I97g==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.7.tgz", + "integrity": "sha512-cW682AxsyP1G+Z0/P7pO/WT2CzYlNxoNe5QejVarW2o5ZxeWSSPAiVEwpEpQR/bUlUmdeWThYTMvBWaopdBsqw==", + "dev": true, + "dependencies": { + "@firebase/app-check": "0.8.0", + "@firebase/app-check-types": "0.5.0", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.0.tgz", + "integrity": "sha512-xAxHPZPIgFXnI+vb4sbBjZcde7ZluzPPaSK7Lx3/nmuVk4TjZvnL8ONnkd4ERQKL8WePQySU+pRcWkh8rDf5Sg==", + "dev": true + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.0.tgz", + "integrity": "sha512-uwSUj32Mlubybw7tedRzR24RP8M8JUVR3NPiMk3/Z4bCmgEKTlQBwMXrehDAZ2wF+TsBq0SN1c6ema71U/JPyQ==", + "dev": true + }, + "node_modules/@firebase/app-compat": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.13.tgz", + "integrity": "sha512-j6ANZaWjeVy5zg6X7uiqh6lM6o3n3LD1+/SJFNs9V781xyryyZWXe+tmnWNWPkP086QfJoNkWN9pMQRqSG4vMg==", + "dev": true, + "dependencies": { + "@firebase/app": "0.9.13", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz", + "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==" + }, + "node_modules/@firebase/auth": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.23.2.tgz", + "integrity": "sha512-dM9iJ0R6tI1JczuGSxXmQbXAgtYie0K4WvKcuyuSTCu9V8eEDiz4tfa1sO3txsfvwg7nOY3AjoCyMYEdqZ8hdg==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.4.2.tgz", + "integrity": "sha512-Q30e77DWXFmXEt5dg5JbqEDpjw9y3/PcP9LslDPR7fARmAOTIY9MM6HXzm9KC+dlrKH/+p6l8g9ifJiam9mc4A==", + "dev": true, + "dependencies": { + "@firebase/auth": "0.23.2", + "@firebase/auth-types": "0.12.0", + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz", + "integrity": "sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg==" + }, + "node_modules/@firebase/auth-types": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.12.0.tgz", + "integrity": "sha512-pPwaZt+SPOshK8xNoiQlK5XIrS97kFYc3Rc7xmy373QsOJ9MmqXxLaYssP5Kcds4wd2qK//amx/c+A8O2fVeZA==", + "dev": true, + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.4.tgz", + "integrity": "sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==", + "dependencies": { + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.14.4.tgz", + "integrity": "sha512-+Ea/IKGwh42jwdjCyzTmeZeLM3oy1h0mFPsTy6OqCWzcu/KFqRAr5Tt1HRCOBlNOdbh84JPZC47WLU18n2VbxQ==", + "dependencies": { + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.3.4.tgz", + "integrity": "sha512-kuAW+l+sLMUKBThnvxvUZ+Q1ZrF/vFJ58iUY9kAcbX48U03nVzIF6Tmkf0p3WVQwMqiXguSgtOPIB6ZCeF+5Gg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/database": "0.14.4", + "@firebase/database-types": "0.10.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.10.4.tgz", + "integrity": "sha512-dPySn0vJ/89ZeBac70T+2tWWPiJXWbmRygYv0smT5TfE3hDrQ09eKMF3Y+vMlTdrMWq7mUdYW5REWPSGH4kAZQ==", + "dependencies": { + "@firebase/app-types": "0.9.0", + "@firebase/util": "1.9.3" + } + }, + "node_modules/@firebase/firestore": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-3.13.0.tgz", + "integrity": "sha512-NwcnU+madJXQ4fbLkGx1bWvL612IJN/qO6bZ6dlPmyf7QRyu5azUosijdAN675r+bOOJxMtP1Bv981bHBXAbUg==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "@firebase/webchannel-wrapper": "0.10.1", + "@grpc/grpc-js": "~1.7.0", + "@grpc/proto-loader": "^0.6.13", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=10.10.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.12.tgz", + "integrity": "sha512-mazuNGAx5Kt9Nph0pm6ULJFp/+j7GSsx+Ncw1GrnKl+ft1CQ4q2LcUssXnjqkX2Ry0fNGqUzC1mfIUrk9bYtjQ==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/firestore": "3.13.0", + "@firebase/firestore-types": "2.5.1", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-2.5.1.tgz", + "integrity": "sha512-xG0CA6EMfYo8YeUxC8FeDzf6W3FX1cLlcAGBYV6Cku12sZRI81oWcu61RSKM66K6kUENP+78Qm8mvroBcm1whw==", + "dev": true, + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/functions": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.10.0.tgz", + "integrity": "sha512-2U+fMNxTYhtwSpkkR6WbBcuNMOVaI7MaH3cZ6UAeNfj7AgEwHwMIFLPpC13YNZhno219F0lfxzTAA0N62ndWzA==", + "dev": true, + "dependencies": { + "@firebase/app-check-interop-types": "0.3.0", + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.4", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.5.tgz", + "integrity": "sha512-uD4jwgwVqdWf6uc3NRKF8cSZ0JwGqSlyhPgackyUPe+GAtnERpS4+Vr66g0b3Gge0ezG4iyHo/EXW/Hjx7QhHw==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/functions": "0.10.0", + "@firebase/functions-types": "0.6.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.0.tgz", + "integrity": "sha512-hfEw5VJtgWXIRf92ImLkgENqpL6IWpYaXVYiRkFY1jJ9+6tIhWM7IzzwbevwIIud/jaxKVdRzD7QBWfPmkwCYw==", + "dev": true + }, + "node_modules/@firebase/installations": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.4.tgz", + "integrity": "sha512-u5y88rtsp7NYkCHC3ElbFBrPtieUybZluXyzl7+4BsIz4sqb4vSAuwHEUgCgCeaQhvsnxDEU6icly8U9zsJigA==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.4.tgz", + "integrity": "sha512-LI9dYjp0aT9Njkn9U4JRrDqQ6KXeAmFbRC0E7jI7+hxl5YmRWysq5qgQl22hcWpTk+cm3es66d/apoDU/A9n6Q==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/installations-types": "0.5.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.0.tgz", + "integrity": "sha512-9DP+RGfzoI2jH7gY4SlzqvZ+hr7gYzPODrbzVD82Y12kScZ6ZpRg/i3j6rleto8vTFC8n6Len4560FnV1w2IRg==", + "dev": true, + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/installations/node_modules/idb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==", + "dev": true + }, + "node_modules/@firebase/logger": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.0.tgz", + "integrity": "sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.4.tgz", + "integrity": "sha512-6JLZct6zUaex4g7HI3QbzeUrg9xcnmDAPTWpkoMpd/GoSVWH98zDoWXMGrcvHeCAIsLpFMe4MPoZkJbrPhaASw==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.4.tgz", + "integrity": "sha512-lyFjeUhIsPRYDPNIkYX1LcZMpoVbBWXX4rPl7c/rqc7G+EUea7IEtSt4MxTvh6fDfPuzLn7+FZADfscC+tNMfg==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/messaging": "0.12.4", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.0.tgz", + "integrity": "sha512-ujA8dcRuVeBixGR9CtegfpU4YmZf3Lt7QYkcj693FFannwNuZgfAYaTmbJ40dtjB81SAu6tbFPL9YLNT15KmOQ==", + "dev": true + }, + "node_modules/@firebase/messaging/node_modules/idb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==", + "dev": true + }, + "node_modules/@firebase/performance": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.6.4.tgz", + "integrity": "sha512-HfTn/bd8mfy/61vEqaBelNiNnvAbUtME2S25A67Nb34zVuCSCRIX4SseXY6zBnOFj3oLisaEqhVcJmVPAej67g==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.4.tgz", + "integrity": "sha512-nnHUb8uP9G8islzcld/k6Bg5RhX62VpbAb/Anj7IXs/hp32Eb2LqFPZK4sy3pKkBUO5wcrlRWQa6wKOxqlUqsg==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/performance": "0.6.4", + "@firebase/performance-types": "0.2.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.0.tgz", + "integrity": "sha512-kYrbr8e/CYr1KLrLYZZt2noNnf+pRwDq2KK9Au9jHrBMnb0/C9X9yWSXmZkFt4UIdsQknBq8uBB7fsybZdOBTA==", + "dev": true + }, + "node_modules/@firebase/remote-config": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.4.4.tgz", + "integrity": "sha512-x1ioTHGX8ZwDSTOVp8PBLv2/wfwKzb4pxi0gFezS5GCJwbLlloUH4YYZHHS83IPxnua8b6l0IXUaWd0RgbWwzQ==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.4.tgz", + "integrity": "sha512-FKiki53jZirrDFkBHglB3C07j5wBpitAaj8kLME6g8Mx+aq7u9P7qfmuSRytiOItADhWUj7O1JIv7n9q87SuwA==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/remote-config": "0.4.4", + "@firebase/remote-config-types": "0.3.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.3.0.tgz", + "integrity": "sha512-RtEH4vdcbXZuZWRZbIRmQVBNsE7VDQpet2qFvq6vwKLBIQRQR5Kh58M4ok3A3US8Sr3rubYnaGqZSurCwI8uMA==", + "dev": true + }, + "node_modules/@firebase/storage": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.11.2.tgz", + "integrity": "sha512-CtvoFaBI4hGXlXbaCHf8humajkbXhs39Nbh6MbNxtwJiCqxPy9iH3D3CCfXAvP0QvAAwmJUTK3+z9a++Kc4nkA==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.2.tgz", + "integrity": "sha512-wvsXlLa9DVOMQJckbDNhXKKxRNNewyUhhbXev3t8kSgoCotd1v3MmqhKKz93ePhDnhHnDs7bYHy+Qa8dRY6BXw==", + "dev": true, + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/storage": "0.11.2", + "@firebase/storage-types": "0.8.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.0.tgz", + "integrity": "sha512-isRHcGrTs9kITJC0AVehHfpraWFui39MPaU7Eo8QfWlqW7YPymBmRgjDrlOgFdURh6Cdeg07zmkLP5tzTKRSpg==", + "dev": true, + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz", + "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.10.1.tgz", + "integrity": "sha512-Dq5rYfEpdeel0bLVN+nfD1VWmzCkK+pJbSjIawGE+RY4+NIJqhbUDDQjvV0NUK84fMfwxvtFoCtEe70HfZjFcw==", + "dev": true + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.27.16", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz", + "integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.6", + "@floating-ui/utils": "^0.2.10", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@google-cloud/firestore": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-6.8.0.tgz", + "integrity": "sha512-JRpk06SmZXLGz0pNx1x7yU3YhkUXheKgH5hbDZ4kMsdhtfV5qPLJLRI4wv69K0cZorIk+zTMOwptue7hizo0eA==", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^3.5.7", + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@google-cloud/firestore/node_modules/protobufjs": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz", + "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@google-cloud/paginator": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.7.tgz", + "integrity": "sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-3.0.0.tgz", + "integrity": "sha512-HRkZsNmjScY6Li8/kb70wjGlDDyLkVk3KvoEo9uIoxSjYLJasGiCch9+PqRVDOCGUFvEIqyogl+BeqILL4OJHA==", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-3.0.1.tgz", + "integrity": "sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@google-cloud/storage": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-6.12.0.tgz", + "integrity": "sha512-78nNAY7iiZ4O/BouWMWTD/oSF2YtYgYB3GZirn0To6eBOugjXVoK+GXgUXOl+HlqbAOyHxAVXOlsj3snfbQ1dw==", + "optional": true, + "dependencies": { + "@google-cloud/paginator": "^3.0.7", + "@google-cloud/projectify": "^3.0.0", + "@google-cloud/promisify": "^3.0.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "compressible": "^2.0.12", + "duplexify": "^4.0.0", + "ent": "^2.2.0", + "extend": "^3.0.2", + "fast-xml-parser": "^4.2.2", + "gaxios": "^5.0.0", + "google-auth-library": "^8.0.1", + "mime": "^3.0.0", + "mime-types": "^2.0.8", + "p-limit": "^3.0.1", + "retry-request": "^5.0.0", + "teeny-request": "^8.0.0", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@google-cloud/storage/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@googleapis/youtube": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@googleapis/youtube/-/youtube-8.0.2.tgz", + "integrity": "sha512-ElWrfTS7NGpnrFeB//XPIo9EgFgrIh/aUQCjfLuhYFn5ikYWXkapn5kNKqGY8ZB03JA+EbSh5mH6F3MXVexOyQ==", + "dependencies": { + "googleapis-common": "^6.0.3" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.3.tgz", + "integrity": "sha512-H9l79u4kJ2PVSxUNA08HMYAnUBLj9v6KjYQ7SQ71hOZcEXhShE/y5iQCesP8+6/Ik/7i2O0a10bPquIcYfufog==", + "dev": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.10.tgz", + "integrity": "sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==", + "dev": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.4", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/grpc-js/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@grpc/grpc-js/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/grpc-js/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/@grpc/grpc-js/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@grpc/grpc-js/node_modules/protobufjs": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz", + "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@grpc/grpc-js/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@grpc/grpc-js/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@grpc/grpc-js/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/grpc-js/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.13.tgz", + "integrity": "sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==", + "dev": true, + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^6.11.3", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@ioredis/commands": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz", + "integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==", + "license": "MIT" + }, + "node_modules/@jsdoc/salty": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.5.tgz", + "integrity": "sha512-TfRP53RqunNe2HBobVBJ0VLhK1HbfvBYeTC1ahnN64PWvyYyGebmMiPkuwvD9fpw2ZbkoPb8Q7mwy0aR8Z9rvw==", + "optional": true, + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v12.0.0" + } + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@lukeed/uuid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@lukeed/uuid/-/uuid-2.0.1.tgz", + "integrity": "sha512-qC72D4+CDdjGqJvkFMMEAtancHUQ7/d/tAiHf64z8MopFDmcrtbcJuerDtFceuAfQJ2pDSfCKCtbqoGBNnwg0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@mantine/core": { + "version": "8.3.10", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.10.tgz", + "integrity": "sha512-aKQFETN14v6GtM07b/G5yJneMM1yrgf9mNrTah6GVy5DvQM0AeutITT7toHqh5gxxwzdg/DoY+HQsv5zhqnc5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.27.16", + "clsx": "^2.1.1", + "react-number-format": "^5.4.4", + "react-remove-scroll": "^2.7.1", + "react-textarea-autosize": "8.5.9", + "type-fest": "^4.41.0" + }, + "peerDependencies": { + "@mantine/hooks": "8.3.10", + "react": "^18.x || ^19.x", + "react-dom": "^18.x || ^19.x" + } + }, + "node_modules/@mantine/core/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@mantine/hooks": { + "version": "8.3.10", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.10.tgz", + "integrity": "sha512-bv+yYHl+keTIvakiDzVJMIjW+o8/Px0G3EdpCMFG+U2ux6SwQqluqoq+/kqrTtT6RaLvQ0fMxjpIULF2cu/xAg==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "react": "^18.x || ^19.x" + } + }, + "node_modules/@pm2/blessed": { + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@pm2/blessed/-/blessed-0.1.81.tgz", + "integrity": "sha512-ZcNHqQjMuNRcQ7Z1zJbFIQZO/BDKV3KbiTckWdfbUaYhj7uNmUwb+FbdDWSCkvxNr9dBJQwvV17o6QBkAvgO0g==", + "license": "MIT", + "bin": { + "blessed": "bin/tput.js" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "devOptional": true + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "devOptional": true + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "devOptional": true + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "devOptional": true + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "devOptional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "devOptional": true + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "devOptional": true + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "devOptional": true + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "devOptional": true + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "devOptional": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v16" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@silentbot1/nat-api": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/@silentbot1/nat-api/-/nat-api-0.4.9.tgz", + "integrity": "sha512-Bm2Fr0sJyGr4B/XgKjQxjGe7Rzs/OlK91OIHsghObxhP3Y4j2y8o7Xjlledu/pxzFEIWaTbZIBSl8ABqoP/WhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chrome-dgram": "^3.0.6", + "cross-fetch-ponyfill": "^1.0.3", + "debug": "^4.4.0", + "default-gateway": "^7.2.2", + "unordered-array-remove": "^1.0.2", + "xml2js": "^0.6.2" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@silentbot1/nat-api/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@silentbot1/nat-api/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, + "node_modules/@stripe/stripe-js": { + "version": "1.54.2", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-1.54.2.tgz", + "integrity": "sha512-R1PwtDvUfs99cAjfuQ/WpwJ3c92+DAMy9xGApjqlWQMj0FKQabUAys2swfTRNzuYAYJh7NqK2dzcYVNkKLEKUg==", + "dev": true + }, + "node_modules/@svta/common-media-library": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@svta/common-media-library/-/common-media-library-0.17.4.tgz", + "integrity": "sha512-nP/KThzQW5FZKdc9V7ICTa9/A7xGw66VQoLPYOEwwMZTTrISp1zIQAX4KAYJw2PN/VPnxJQJXIYbzZTXgMHctw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@tabler/icons": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.35.0.tgz", + "integrity": "sha512-yYXe+gJ56xlZFiXwV9zVoe3FWCGuZ/D7/G4ZIlDtGxSx5CGQK110wrnT29gUj52kEZoxqF7oURTk97GQxELOFQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + } + }, + "node_modules/@tabler/icons-react": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.35.0.tgz", + "integrity": "sha512-XG7t2DYf3DyHT5jxFNp5xyLVbL4hMJYJhiSdHADzAjLRYfL7AnjlRfiHDHeXxkb2N103rEIvTsBRazxXtAUz2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tabler/icons": "3.35.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + }, + "peerDependencies": { + "react": ">= 16" + } + }, + "node_modules/@thaunknown/simple-peer": { + "version": "10.0.12", + "resolved": "https://registry.npmjs.org/@thaunknown/simple-peer/-/simple-peer-10.0.12.tgz", + "integrity": "sha512-sDrkkOdzlJL8+FXQqYcBb2THHQU+Yrar92SjfW4ZLs877/4QA2kFejuA6DVepsoMpoIbXShc7OCXCwYt4AtGdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.7", + "err-code": "^3.0.1", + "streamx": "^2.20.1", + "uint8-util": "^2.2.5", + "webrtc-polyfill": "^1.1.10" + } + }, + "node_modules/@thaunknown/simple-peer/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@thaunknown/simple-peer/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@thaunknown/simple-websocket": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/@thaunknown/simple-websocket/-/simple-websocket-9.1.3.tgz", + "integrity": "sha512-pf/FCJsgWtLJiJmIpiSI7acOZVq3bIQCpnNo222UFc8Ph1lOUOTpe6LoYhhiOSKB9GUaWJEVUtZ+sK1/aBgU5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "queue-microtask": "^1.2.3", + "streamx": "^2.17.0", + "uint8-util": "^2.2.5", + "ws": "^8.17.1" + } + }, + "node_modules/@thaunknown/simple-websocket/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@thaunknown/simple-websocket/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@thaunknown/thirty-two": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@thaunknown/thirty-two/-/thirty-two-1.0.5.tgz", + "integrity": "sha512-Q53KyCXweV1CS62EfqtPDqfpksn5keQ59PGqzzkK+g8Vif1jB4inoBCcs/BUSdsqddhE3G+2Fn+4RX3S6RqT0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "uint8-util": "^2.2.5" + }, + "engines": { + "node": ">=0.2.6" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/bittorrent-protocol": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@types/bittorrent-protocol/-/bittorrent-protocol-3.1.5.tgz", + "integrity": "sha512-dxBWCVOsun/MqkMSFAlQKW1i88xW8BQmSSKjd8FKUgwaoTh51dgm90fJ0pU1hbAzK0fUc/SJFO2UzKnZDPB0Mw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "dev": true + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.11.tgz", + "integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==", + "dev": true + }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "dev": true, + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==", + "dev": true + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/events-alias": { + "name": "@types/events", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz", + "integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz", + "integrity": "sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", + "optional": true, + "dependencies": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, + "node_modules/@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", + "dev": true + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", + "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz", + "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==", + "optional": true + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "devOptional": true + }, + "node_modules/@types/magnet-uri": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@types/magnet-uri/-/magnet-uri-5.1.5.tgz", + "integrity": "sha512-SbBjlb1KGe38VfjRR+mwqztJd/4skhdKkRbIzPDhTy7IAeEAPZWIVSEkZw00Qr4ZZOGR3/ATJ20WWPBfrKHGdA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz", + "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==", + "optional": true + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "optional": true + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "dev": true + }, + "node_modules/@types/node": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.1.tgz", + "integrity": "sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/node/node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/@types/parse-torrent": { + "version": "5.8.7", + "resolved": "https://registry.npmjs.org/@types/parse-torrent/-/parse-torrent-5.8.7.tgz", + "integrity": "sha512-vZtYe450hO+KL7B5fejM8CHWg1LPZKeVXlolphPsWf6n4H0ZUlI6ICbqHoaFmH7JQmU2yRbGgyvqqizdFuGPFQ==", + "dev": true, + "dependencies": { + "@types/magnet-uri": "*", + "@types/node": "*", + "@types/parse-torrent-file": "*" + } + }, + "node_modules/@types/parse-torrent-file": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@types/parse-torrent-file/-/parse-torrent-file-4.0.6.tgz", + "integrity": "sha512-SxqVth0Iv0WuEkqWS5MaY4S4Tlyi+QHkElQREvsUPw2xHcPgKyQ2dkJRRv5vAxmLzH+tnMdOj1Nws/wsenbzUw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/pg": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.16.0.tgz", + "integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.10", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.10.tgz", + "integrity": "sha512-mxSnDQxPqsZxmeShFH+uwQ4kO4gcJcGahjjMFeLbKE95IAZiiZyiEepGZjtXJ7hN/yfu0bu9xN2ajcU0JcxX6A==", + "dev": true + }, + "node_modules/@types/qs": { + "version": "6.9.17", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", + "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/react-onclickoutside": { + "version": "6.7.10", + "resolved": "https://registry.npmjs.org/@types/react-onclickoutside/-/react-onclickoutside-6.7.10.tgz", + "integrity": "sha512-Do2eOuqlJ9amXAuQO5gbhp5MAPnzZ7cknmYqX4U44tX22eAAnHgQKjp3SaNhSAuUHBTANWEqn1N+nWd3ea8FyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-router": { + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", + "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", + "dev": true, + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "dev": true, + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==", + "optional": true, + "dependencies": { + "@types/glob": "*", + "@types/node": "*" + } + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/simple-peer": { + "version": "9.11.8", + "resolved": "https://registry.npmjs.org/@types/simple-peer/-/simple-peer-9.11.8.tgz", + "integrity": "sha512-rvqefdp2rvIA6wiomMgKWd2UZNPe6LM2EV5AuY3CPQJF+8TbdrL5TjYdMf0VAjGczzlkH4l1NjDkihwbj3Xodw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/webtorrent": { + "version": "0.110.1", + "resolved": "https://registry.npmjs.org/@types/webtorrent/-/webtorrent-0.110.1.tgz", + "integrity": "sha512-fyW/LbaphpGWXdmmuXuerUb+dba/VlWAunMI7MLk44wbA27yIeSHZGuprf4FvXm9M9iqnLu1fmiPlMitg5nhJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bittorrent-protocol": "*", + "@types/node": "*", + "@types/parse-torrent": "*", + "@types/simple-peer": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/youtube": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/youtube/-/youtube-0.1.2.tgz", + "integrity": "sha512-n1/KqusanheyQRWHamNZv8K3kydlRqyEsZEKxMTeNWXQTC15lZprITCUt+WgL1vAIvKHCjPBIWz/gf/KQLsB3g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz", + "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@webtorrent/http-node": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@webtorrent/http-node/-/http-node-1.3.0.tgz", + "integrity": "sha512-GWZQKroPES4z91Ijx6zsOsb7+USOxjy66s8AoTWg0HiBBdfnbtf9aeh3Uav0MgYn4BL8Q7tVSUpd0gGpngKGEQ==", + "dev": true, + "dependencies": { + "freelist": "^1.0.3", + "http-parser-js": "^0.4.3" + } + }, + "node_modules/@webtorrent/http-node/node_modules/http-parser-js": { + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.13.tgz", + "integrity": "sha512-u8u5ZaG0Tr/VvHlucK2ufMuOp4/5bvwgneXle+y228K5rMbJOlVjThONcaAw3ikAy8b2OO9RfEucdMHFz3UWMA==", + "dev": true + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "devOptional": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "optional": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "optional": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/addr-to-ip-port": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/addr-to-ip-port/-/addr-to-ip-port-2.0.0.tgz", + "integrity": "sha512-9bYbtjamtdLHZSqVIUXhilOryNPiL+x+Q5J/Unpg4VY3ZIkK3fT52UoErj1NdUeVm3J1t2iBEAur4Ywbl/bahw==", + "dev": true, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "engines": { + "node": ">=8" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "optional": true, + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/awaitqueue": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/awaitqueue/-/awaitqueue-3.3.0.tgz", + "integrity": "sha512-zLxDhzQbzHmOyvxi7g3OlfR7jLrcmElStPxfLPpJkrFSDm71RSrY/MvsDA8Btlx8X1nOHUzGhQvc6bdUjL2f2w==", + "dev": true, + "license": "ISC", + "dependencies": { + "debug": "^4.4.3" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mediasoup" + } + }, + "node_modules/awaitqueue/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/awaitqueue/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT", + "optional": true + }, + "node_modules/bare-addon-resolve": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.9.4.tgz", + "integrity": "sha512-unn6Vy/Yke6F99vg/7tcrvM2KUvIhTNniaSqDbam4AWkd4NhvDVSrQiRYVlNzUV2P7SPobkCK7JFVxrJk9btCg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-module-resolve": "^1.10.0", + "bare-semver": "^1.0.0" + }, + "peerDependencies": { + "bare-url": "*" + }, + "peerDependenciesMeta": { + "bare-url": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.7.0.tgz", + "integrity": "sha512-b3N5eTW1g7vXkw+0CXh/HazGTcO5KYuu/RCNaJbDMPI6LHDi+7qe8EmxKUVe1sUbY2KZOVZFyj62x0OEz9qyAA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bare-fs": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.4.4.tgz", + "integrity": "sha512-Q8yxM1eLhJfuM7KXVP3zjhBvtMJCYRByoTT+wHXjpdMELv0xICFJX+1w4c7csa+WZEOsq4ItJ4RGwvzid6m/dw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-module-resolve": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.11.1.tgz", + "integrity": "sha512-DCxeT9i8sTs3vUMA3w321OX/oXtNEu5EjObQOnTmCdNp5RXHBAvAaBDHvAi9ta0q/948QPz+co6SsGi6aQMYRg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-semver": "^1.0.0" + }, + "peerDependencies": { + "bare-url": "*" + }, + "peerDependenciesMeta": { + "bare-url": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", + "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-semver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.1.tgz", + "integrity": "sha512-UtggzHLiTrmFOC/ogQ+Hy7VfoKoIwrP1UFcYtTxoCUdLtsIErT8+SWtOC2DH/snT9h+xDrcBEPcwKei1mzemgg==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/bare-stream": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", + "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.2.2.tgz", + "integrity": "sha512-g+ueNGKkrjMazDG3elZO1pNs3HY5+mMmOet1jtKyhOaCnkLzitxf26z7hoAEkDNgdNmnc1KIlt/dw6Po6xZMpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/basic-ftp": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", + "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/bcp-47": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.0.tgz", + "integrity": "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47-match": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", + "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47-normalize": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/bcp-47-normalize/-/bcp-47-normalize-2.3.0.tgz", + "integrity": "sha512-8I/wfzqQvttUFz7HVJgIZ7+dj3vUaIyIxYXaTRP1YWoSDfzt6TUmxaKZeuXR62qBmYr+nvuWINFRl6pZ5DlN4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bcp-47": "^2.0.0", + "bcp-47-match": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bencode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bencode/-/bencode-4.0.0.tgz", + "integrity": "sha512-AERXw18df0pF3ziGOCyUjqKZBVNH8HV3lBxnx5w0qtgMIk4a1wb9BkcCQbkp9Zstfrn/dzRwl7MmUHHocX3sRQ==", + "dev": true, + "dependencies": { + "uint8-util": "^2.2.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/bep53-range": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bep53-range/-/bep53-range-2.0.0.tgz", + "integrity": "sha512-sMm2sV5PRs0YOVk0LTKtjuIprVzxgTQUsrGX/7Yph2Rm4FO2Fqqtq7hNjsOB5xezM4v4+5rljCgK++UeQJZguA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "engines": { + "node": "*" + } + }, + "node_modules/bitfield": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/bitfield/-/bitfield-4.2.0.tgz", + "integrity": "sha512-kUTatQb/mBd8uhvdLrUkouGDBUQiJaIOvPlptUwOWp6MFqih4d1MiVf0m3ATxfZSzu+LjW/awFeABltYa62uIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/bittorrent-dht": { + "version": "11.0.11", + "resolved": "https://registry.npmjs.org/bittorrent-dht/-/bittorrent-dht-11.0.11.tgz", + "integrity": "sha512-5rWMoK/2XjcPSx9nfqiL6mHxsXLwohX+81aL4a3qUyGU1992mBqARQE/evZ+a6eWb5DeRjeDU+qFZm11rmPZnQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "bencode": "^4.0.0", + "debug": "^4.4.3", + "k-bucket": "^5.1.0", + "k-rpc": "^5.1.0", + "last-one-wins": "^1.0.4", + "lru": "^3.1.0", + "randombytes": "^2.1.0", + "record-cache": "^1.2.0" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/bittorrent-dht/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/bittorrent-dht/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/bittorrent-lsd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bittorrent-lsd/-/bittorrent-lsd-2.0.0.tgz", + "integrity": "sha512-jV+SMTGNY1iGWjf5cPA2HMeA6axuMQRWwWELtsuZ1FmQmZwC74we92nwtDTfv1WMnLx+oqEjWRri42IHjZitSQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "chrome-dgram": "^3.0.6", + "debug": "^4.2.0" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/bittorrent-lsd/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/bittorrent-lsd/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/bittorrent-peerid": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/bittorrent-peerid/-/bittorrent-peerid-1.3.6.tgz", + "integrity": "sha512-VyLcUjVMEOdSpHaCG/7odvCdLbAB1y3l9A2V6WIje24uV7FkJPrQrH/RrlFmKxP89pFVDEnE+YlHaFujlFIZsg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bittorrent-protocol": { + "version": "4.1.21", + "resolved": "https://registry.npmjs.org/bittorrent-protocol/-/bittorrent-protocol-4.1.21.tgz", + "integrity": "sha512-CcuPt6BL7gXa8BF+0GckYcQmr44ARfSPM0rYwMeYgWg+jftekWgy5vuxX6wJDpC5bKFvqNG+74bPBjyM7Swxrw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "bencode": "^4.0.0", + "bitfield": "^4.2.0", + "debug": "^4.4.3", + "rc4": "^0.1.5", + "streamx": "^2.22.1", + "throughput": "^1.0.2", + "uint8-util": "^2.2.5", + "unordered-array-remove": "^1.0.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/bittorrent-protocol/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/bittorrent-protocol/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/bittorrent-tracker": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/bittorrent-tracker/-/bittorrent-tracker-11.2.2.tgz", + "integrity": "sha512-pVjpd6tPtLByrYwtDOo+cVx9zQZ2XUvlaWrlm57+9yvVDKHuNL+TFEAtyfXuIutghG7Bde/uWXGfoVWpPYY+8A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "@thaunknown/simple-peer": "^10.0.8", + "@thaunknown/simple-websocket": "^9.1.3", + "bencode": "^4.0.0", + "bittorrent-peerid": "^1.3.6", + "chrome-dgram": "^3.0.6", + "compact2string": "^1.4.1", + "cross-fetch-ponyfill": "^1.0.3", + "debug": "^4.3.4", + "ip": "^2.0.1", + "lru": "^3.1.0", + "minimist": "^1.2.8", + "once": "^1.4.0", + "queue-microtask": "^1.2.3", + "random-iterate": "^1.0.1", + "run-parallel": "^1.2.0", + "run-series": "^1.1.9", + "socks": "^2.8.3", + "string2compact": "^2.0.1", + "uint8-util": "^2.2.5", + "unordered-array-remove": "^1.0.2", + "ws": "^8.17.0" + }, + "bin": { + "bittorrent-tracker": "bin/cmd.js" + }, + "engines": { + "node": ">=16.0.0" + }, + "optionalDependencies": { + "bufferutil": "^4.0.8", + "utf-8-validate": "^6.0.4" + } + }, + "node_modules/bittorrent-tracker/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/bittorrent-tracker/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/bittorrent-tracker/node_modules/utf-8-validate": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.5.tgz", + "integrity": "sha512-EYZR+OpIXp9Y1eG1iueg8KRsY8TuT8VNgnanZ0uA3STqhHQTLwbl+WX76/9X5OY12yQubymBpaBSmMPkSTQcKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/block-iterator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/block-iterator/-/block-iterator-1.1.1.tgz", + "integrity": "sha512-DrjdVWZemVO4iBf4tiOXjUrY5cNesjzy0t7sIiu2rdl8cOCHRxAgKjSJFc3vBZYYMMmshUAxajl8QQh/uxXTKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "optional": true + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buildcheck": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", + "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cache-chunk-store": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/cache-chunk-store/-/cache-chunk-store-3.2.2.tgz", + "integrity": "sha512-2lJdWbgHFFxcSth9s2wpId3CR3v1YC63KjP4T9WhpW7LWlY7Hiiei3QwwqzkWqlJTfR8lSy9F5kRQECeyj+yQA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "lru": "^3.1.0", + "queue-microtask": "^1.2.3" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/can-autoplay": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/can-autoplay/-/can-autoplay-3.0.2.tgz", + "integrity": "sha512-Ih6wc7yJB4TylS/mLyAW0Dj5Nh3Gftq/g966TcxgvpNCOzlbqTs85srAq7mwIspo4w8gnLCVVGroyCHfh6l9aA==", + "dev": true + }, + "node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "optional": true, + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "optional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/chrome-dgram": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/chrome-dgram/-/chrome-dgram-3.0.6.tgz", + "integrity": "sha512-bqBsUuaOiXiqxXt/zA/jukNJJ4oaOtc7ciwqJpZVEaaXwwxqgI2/ZdG02vXYWUhHGziDlvGMQWk0qObgJwVYKA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "run-series": "^1.1.9" + } + }, + "node_modules/chrome-dns": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chrome-dns/-/chrome-dns-1.0.1.tgz", + "integrity": "sha512-HqsYJgIc8ljJJOqOzLphjAs79EUuWSX3nzZi2LNkzlw3GIzAeZbaSektC8iT/tKvLqZq8yl1GJu5o6doA4TRbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chrome-net": "^3.3.2" + } + }, + "node_modules/chrome-net": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/chrome-net/-/chrome-net-3.3.4.tgz", + "integrity": "sha512-Jzy2EnzmE+ligqIZUsmWnck9RBXLuUy6CaKyuNMtowFG3ZvLt8d+WBJCTPEludV0DHpIKjAOlwjFmTaEdfdWCw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1" + } + }, + "node_modules/chunk-store-iterator": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chunk-store-iterator/-/chunk-store-iterator-1.0.4.tgz", + "integrity": "sha512-LGjzJNmk7W1mrdaBoJNztPumT2ACmgjHmI1AMm8aeGYOl4+LKaYC/yfnx27i++LiAtoe/dR+3jC8HRzb6gW4/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "block-iterator": "^1.1.1" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/codem-isoboxer": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/codem-isoboxer/-/codem-isoboxer-0.3.10.tgz", + "integrity": "sha512-eNk3TRV+xQMJ1PEj0FQGY8KD4m0GPxT487XJ+Iftm7mVa9WpPFDMWqPt+46buiP5j5Wzqe5oMIhqBcAeKfygSA==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compact2string": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/compact2string/-/compact2string-1.4.1.tgz", + "integrity": "sha512-3D+EY5nsRhqnOwDxveBv5T8wGo4DEvYxjDtPGmdOX+gfr5gE92c2RC0w2wa+xEefm07QuVqqcF3nZJUZ92l/og==", + "dev": true, + "dependencies": { + "ipaddr.js": ">= 0.1.5" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/countup.js": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/countup.js/-/countup.js-2.9.0.tgz", + "integrity": "sha512-llqrvyXztRFPp6+i8jx25phHWcVWhrHO4Nlt0uAOSKHB8778zzQswa4MU3qKBvkXfJKftRYFJuVHez67lyKdHg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cpu-features": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.9.tgz", + "integrity": "sha512-AKjgn2rP2yJyfbepsmLfiYcmtNn/2eUvocUyM/09yB0YDiz39HteK/5/T4Onf0pmdYDMgkBoGvRLvEguzyL7wQ==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.17.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/cpus": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cpus/-/cpus-1.0.3.tgz", + "integrity": "sha512-PXHBvGLuL69u55IkLa5e5838fLhIMHxmkV4ge42a8alGyn7BtawYgI0hQ849EedvtHIOLNNH3i6eQU1BiE9SUA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/create-torrent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/create-torrent/-/create-torrent-6.1.0.tgz", + "integrity": "sha512-War593HCsg4TotHgMGWTJqnDHN0pmEU2RM13xUzzSZ78TpRNOC2bbcsC5yMO3pqIkedHEWFzYNqH1yhwuuBYTg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "bencode": "^4.0.0", + "block-iterator": "^1.1.1", + "fast-readable-async-iterator": "^2.0.0", + "is-file": "^1.0.0", + "join-async-iterator": "^1.1.1", + "junk": "^4.0.1", + "minimist": "^1.2.8", + "once": "^1.4.0", + "piece-length": "^2.0.1", + "queue-microtask": "^1.2.3", + "run-parallel": "^1.2.0", + "uint8-util": "^2.2.5" + }, + "bin": { + "create-torrent": "bin/cmd.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cross-fetch-ponyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cross-fetch-ponyfill/-/cross-fetch-ponyfill-1.0.3.tgz", + "integrity": "sha512-uOBkDhUAGAbx/FEzNKkOfx3w57H8xReBBXoZvUnOKTI0FW0Xvrj3GrYv2iZXUqlffC1LMGfQzhmBM/ke+6eTDA==", + "dev": true, + "dependencies": { + "abort-controller": "^3.0.0", + "node-fetch": "^3.3.0" + } + }, + "node_modules/cross-fetch-ponyfill/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/cross-fetch-ponyfill/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dev": true, + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dev": true, + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dev": true, + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dev": true, + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dev": true, + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dev": true, + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/dashjs": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dashjs/-/dashjs-5.1.0.tgz", + "integrity": "sha512-FilZfs+0pj9NB7q2VMT4zahG+V2JoleVl6K9kWunvndICdclw/jLAfLImcmCr1WqxH4hsgsFXvaVgea9XGkgVQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@svta/common-media-library": "^0.17.1", + "bcp-47-match": "^2.0.3", + "bcp-47-normalize": "^2.3.0", + "codem-isoboxer": "0.3.10", + "fast-deep-equal": "3.1.3", + "html-entities": "^2.5.2", + "imsc": "^1.1.5", + "localforage": "^1.10.0", + "path-browserify": "^1.0.1", + "ua-parser-js": "^1.0.37" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "dev": true + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "optional": true + }, + "node_modules/default-gateway": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-7.2.2.tgz", + "integrity": "sha512-AD7TrdNNPXRZIGw63dw+lnGmT4v7ggZC5NHNJgAYWm5njrwoze1q5JSAW9YuLy2tjnoLUG/r8FEB93MCh9QJPg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "execa": "^7.1.1" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.0.tgz", + "integrity": "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/discord-api-types": { + "version": "0.38.34", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.34.tgz", + "integrity": "sha512-muq7xKGznj5MSFCzuIm/2TO7DpttuomUTemVM82fRqgnMl70YRzEyY24jlbiV6R9tzOTq6A6UnZ0bsfZeKD38Q==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/discord.js": { + "version": "14.25.1", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.25.1.tgz", + "integrity": "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/builders": "^1.13.0", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.6.2", + "@discordjs/rest": "^2.6.0", + "@discordjs/util": "^1.2.0", + "@discordjs/ws": "^1.2.3", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "^0.38.33", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "optional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/emoji-mart": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.5.2.tgz", + "integrity": "sha512-Sqc/nso4cjxhOwWJsp9xkVm8OF5c+mJLZJFoFfzRuKO+yWiN7K8c96xmtughYb0d/fZ8UC6cLIQ/p4BR6Pv3/A==", + "dev": true, + "peer": true + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "devOptional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-client/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", + "optional": true + }, + "node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "optional": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/err-code": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-3.0.1.tgz", + "integrity": "sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "optional": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "optional": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/events-alias": { + "name": "events", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/body-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/express/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", + "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.0.tgz", + "integrity": "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==", + "dependencies": { + "mime-db": "^1.53.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/express/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/express/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/fake-mediastreamtrack": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/fake-mediastreamtrack/-/fake-mediastreamtrack-2.2.1.tgz", + "integrity": "sha512-SITLc7UTDirSdgLGORrlmqjWLJtbtfIz/xO7DwVbJH3f0ds+NQok4ccl/WEzz49NSgiUlXf2wDW0+y5C+TO6EA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@lukeed/uuid": "^2.0.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-equals": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.2.tgz", + "integrity": "sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "optional": true + }, + "node_modules/fast-readable-async-iterator": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-readable-async-iterator/-/fast-readable-async-iterator-2.0.0.tgz", + "integrity": "sha512-8Sld+DuyWRIftl86ZguJxR2oXCBccOiJxrY/Rj9/7ZBynW8pYMWzIcqxFL1da+25jaWJZVa+HHX/8SsA21JdTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-text-encoding": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", + "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==" + }, + "node_modules/fast-xml-parser": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.4.tgz", + "integrity": "sha512-jE8ugADnYOBsu1uaoayVl1tVKAMNOXyjwvv2U6udEA2ORBhDooJDWoGxTkhd4Qn4yh59JVVt/pKXtjPwx9OguQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/filename-reserved-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz", + "integrity": "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/firebase": { + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-9.23.0.tgz", + "integrity": "sha512-/4lUVY0lUvBDIaeY1q6dUYhS8Sd18Qb9CgWkPZICUo9IXpJNCEagfNZXBBFCkMTTN5L5gx2Hjr27y21a9NzUcA==", + "dev": true, + "dependencies": { + "@firebase/analytics": "0.10.0", + "@firebase/analytics-compat": "0.2.6", + "@firebase/app": "0.9.13", + "@firebase/app-check": "0.8.0", + "@firebase/app-check-compat": "0.3.7", + "@firebase/app-compat": "0.2.13", + "@firebase/app-types": "0.9.0", + "@firebase/auth": "0.23.2", + "@firebase/auth-compat": "0.4.2", + "@firebase/database": "0.14.4", + "@firebase/database-compat": "0.3.4", + "@firebase/firestore": "3.13.0", + "@firebase/firestore-compat": "0.3.12", + "@firebase/functions": "0.10.0", + "@firebase/functions-compat": "0.3.5", + "@firebase/installations": "0.6.4", + "@firebase/installations-compat": "0.2.4", + "@firebase/messaging": "0.12.4", + "@firebase/messaging-compat": "0.2.4", + "@firebase/performance": "0.6.4", + "@firebase/performance-compat": "0.2.4", + "@firebase/remote-config": "0.4.4", + "@firebase/remote-config-compat": "0.2.4", + "@firebase/storage": "0.11.2", + "@firebase/storage-compat": "0.3.2", + "@firebase/util": "1.9.3" + } + }, + "node_modules/firebase-admin": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-11.11.1.tgz", + "integrity": "sha512-UyEbq+3u6jWzCYbUntv/HuJiTixwh36G1R9j0v71mSvGAx/YZEWEW7uSGLYxBYE6ckVRQoKMr40PYUEzrm/4dg==", + "dependencies": { + "@fastify/busboy": "^1.2.1", + "@firebase/database-compat": "^0.3.4", + "@firebase/database-types": "^0.10.4", + "@types/node": ">=12.12.47", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^3.0.1", + "node-forge": "^1.3.1", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^6.8.0", + "@google-cloud/storage": "^6.9.5" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/freelist": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/freelist/-/freelist-1.0.3.tgz", + "integrity": "sha512-Ji7fEnMdZDGbS5oXElpRJsn9jPvBR8h/037D3bzreNmS8809cISq/2D9//JbA/TaZmkkN8cmecXwmQHmM+NHhg==", + "dev": true + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-chunk-store": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/fs-chunk-store/-/fs-chunk-store-5.0.0.tgz", + "integrity": "sha512-tKlT0joU9KmsLn0dTbVYVUa7VNqYQhl0X2qPPsN9lPEc3guXOmQJWY5/7kpo34Sk273qyWT5mqEhROCQPF+JKw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^3.0.0", + "queue-microtask": "^1.2.2", + "random-access-file": "^4.0.0", + "run-parallel": "^1.1.2", + "thunky": "^1.0.1", + "uint8-util": "^2.2.5" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, + "node_modules/fs-native-extensions": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/fs-native-extensions/-/fs-native-extensions-1.4.4.tgz", + "integrity": "sha512-iLo3r2ei97thJNoj3DgSdzUF2hZ2yekZpXF98LlHc2eZGPOwiVblyEa6iS68zLu9ayXvlE8/c3CMaagNbHJB1Q==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "require-addon": "^1.1.0", + "which-runtime": "^1.2.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "optional": true + }, + "node_modules/fsa-chunk-store": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fsa-chunk-store/-/fsa-chunk-store-1.3.0.tgz", + "integrity": "sha512-0WCfuxqqSB6Tz/g7Ar/nwAxMoigXaIXuvfrnLIEFYIA9uc6w9eNaHuBGzU1X3lyM4cpLKCOTUmKAA/gCiTvzMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^3.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "optional": true + }, + "node_modules/gaxios": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", + "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gaxios/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/gcp-metadata": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", + "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "devOptional": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", + "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "dunder-proto": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-stdin": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", + "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/google-auth-library": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.9.0.tgz", + "integrity": "sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg==", + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.3.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-gax": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-3.6.1.tgz", + "integrity": "sha512-g/lcUjGcB6DSw2HxgEmCDOrI/CByOwqRvsuUvNalHUK2iPPPlmAIpbMbl62u0YufGMr8zgE3JL7th6dCb1Ry+w==", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "~1.8.0", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "@types/rimraf": "^3.0.2", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^8.0.2", + "is-stream-ended": "^0.1.4", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^1.0.0", + "protobufjs": "7.2.4", + "protobufjs-cli": "1.1.1", + "retry-request": "^5.0.0" + }, + "bin": { + "compileProtos": "build/tools/compileProtos.js", + "minifyProtoJson": "build/tools/minify.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-gax/node_modules/@grpc/grpc-js": { + "version": "1.8.22", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.22.tgz", + "integrity": "sha512-oAjDdN7fzbUi+4hZjKG96MR6KTEubAeMpQEb+77qy+3r0Ua5xTFuie6JOLr4ZZgl5g+W5/uRTS2M1V8mVAFPuA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/google-gax/node_modules/@grpc/proto-loader": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.10.tgz", + "integrity": "sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==", + "optional": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.4", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/google-gax/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/google-gax/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "optional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-gax/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "optional": true + }, + "node_modules/google-gax/node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "optional": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/google-gax/node_modules/escodegen/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "optional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/google-gax/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-gax/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/google-gax/node_modules/protobufjs": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz", + "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/google-gax/node_modules/protobufjs-cli": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.1.tgz", + "integrity": "sha512-VPWMgIcRNyQwWUv8OLPyGQ/0lQY/QTQAVN5fh+XzfDwsVw1FZ2L3DM/bcBf8WPiRz2tNpaov9lPZfNcmNo6LXA==", + "optional": true, + "dependencies": { + "chalk": "^4.0.0", + "escodegen": "^1.13.0", + "espree": "^9.0.0", + "estraverse": "^5.1.0", + "glob": "^8.0.0", + "jsdoc": "^4.0.0", + "minimist": "^1.2.0", + "semver": "^7.1.2", + "tmp": "^0.2.1", + "uglify-js": "^3.7.7" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "protobufjs": "^7.0.0" + } + }, + "node_modules/google-gax/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/google-gax/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/google-gax/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/google-gax/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "optional": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-gax/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/googleapis-common": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-6.0.4.tgz", + "integrity": "sha512-m4ErxGE8unR1z0VajT6AYk3s6a9gIMM6EkDZfkPnES8joeOlEtFEJeF8IyZkb0tjPXkktUfYrE4b3Li1DNyOwA==", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^5.0.1", + "google-auth-library": "^8.0.2", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "optional": true + }, + "node_modules/gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "dependencies": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/h264-profile-level-id": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/h264-profile-level-id/-/h264-profile-level-id-2.3.1.tgz", + "integrity": "sha512-x2P85PdoRsjCQE7sZjmQQDEf61vTTxOSPNFyqevXaiiq6DzvLVnd0VBq/C7wplrYEqTXvMW5CLIDUMBekNN4Vw==", + "dev": true, + "license": "ISC", + "dependencies": { + "debug": "^4.4.3" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mediasoup" + } + }, + "node_modules/h264-profile-level-id/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/h264-profile-level-id/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + }, + "node_modules/hls.js": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz", + "integrity": "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dev": true, + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "dev": true + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true + }, + "node_modules/immediate-chunk-store": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/immediate-chunk-store/-/immediate-chunk-store-2.2.0.tgz", + "integrity": "sha512-1bHBna0hCa6arRXicu91IiL9RvvkbNYLVq+mzWdaLGZC3hXvX4doh8e1dLhMKez5siu63CYgO5NrGJbRX5lbPA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.3" + } + }, + "node_modules/imsc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/imsc/-/imsc-1.1.5.tgz", + "integrity": "sha512-V8je+CGkcvGhgl2C1GlhqFFiUOIEdwXbXLiu1Fcubvvbo+g9inauqT3l0pNYXGoLPBj3jxtZz9t+wCopMkwadQ==", + "dev": true, + "dependencies": { + "sax": "1.2.1" + } + }, + "node_modules/imsc/node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/ioredis": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz", + "integrity": "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.4.0", + "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" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ioredis/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ioredis/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/ip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", + "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-set": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ip-set/-/ip-set-2.2.0.tgz", + "integrity": "sha512-NmmY3BfY4pejh6GOqNcNWRsBNdR+I7pUVtXRgZlkZdcnLtlG4X6HNtu2FZoCGyvGRzyroP1fJ+SJZBZ65JJl/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip": "^2.0.1" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-file/-/is-file-1.0.0.tgz", + "integrity": "sha512-ZGMuc+xA8mRnrXtmtf2l/EkIW2zaD2LSBWlaOVEF6yH4RTndHob65V4SwWWdtGKVthQfXPVKsXqw4TDUjbVxVQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==", + "optional": true + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/join-async-iterator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/join-async-iterator/-/join-async-iterator-1.1.1.tgz", + "integrity": "sha512-ATse+nuNeKZ9K1y27LKdvPe/GCe9R/u9dw9vI248e+vILeRK3IcJP4JUPAlSmKRCDK0cKhEwfmiw4Skqx7UnGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "optional": true, + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/jsdoc": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.2.tgz", + "integrity": "sha512-e8cIg2z62InH7azBBi3EsSEqrKx+nUtAS5bBcYTSpZFA+vhNPyhv8PTFZ0WsjOPDj04/dOLlm08EDcQJDqaGQg==", + "optional": true, + "dependencies": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^12.2.3", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^12.3.2", + "markdown-it-anchor": "^8.4.1", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsdoc/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jsdoc/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/jws": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.2", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/junk": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/junk/-/junk-4.0.1.tgz", + "integrity": "sha512-Qush0uP+G8ZScpGMZvHUiRfI0YBWuB3gVBYlI0v0vvOJt5FLicco+IkP0a50LqTTQhmts/m6tP5SWE+USyIvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwks-rsa": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz", + "integrity": "sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==", + "dependencies": { + "@types/express": "^4.17.17", + "@types/jsonwebtoken": "^9.0.2", + "debug": "^4.3.4", + "jose": "^4.14.6", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jwks-rsa/node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/jwks-rsa/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/jwks-rsa/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/jwks-rsa/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/k-bucket": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/k-bucket/-/k-bucket-5.1.0.tgz", + "integrity": "sha512-Fac7iINEovXIWU20GPnOMLUbjctiS+cnmyjC4zAUgvs3XPf1vo9akfCHkigftSic/jiKqKl+KA3a/vFcJbHyCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/k-rpc": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/k-rpc/-/k-rpc-5.1.0.tgz", + "integrity": "sha512-FGc+n70Hcjoa/X2JTwP+jMIOpBz+pkRffHnSl9yrYiwUxg3FIgD50+u1ePfJUOnRCnx6pbjmVk5aAeB1wIijuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "k-bucket": "^5.0.0", + "k-rpc-socket": "^1.7.2", + "randombytes": "^2.0.5" + } + }, + "node_modules/k-rpc-socket": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/k-rpc-socket/-/k-rpc-socket-1.11.1.tgz", + "integrity": "sha512-8xtA8oqbZ6v1Niryp2/g4GxW16EQh5MvrUylQoOG+zcrDff5CKttON2XUXvMwlIHq4/2zfPVFiinAccJ+WhxoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bencode": "^2.0.0", + "chrome-dgram": "^3.0.2", + "chrome-dns": "^1.0.0", + "chrome-net": "^3.3.2" + } + }, + "node_modules/k-rpc-socket/node_modules/bencode": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/bencode/-/bencode-2.0.3.tgz", + "integrity": "sha512-D/vrAD4dLVX23NalHwb8dSvsUsxeRPO8Y7ToKA015JQYq69MLDOMkC0uGZYA/MPpltLO8rt8eqFC2j8DxjTZ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "optional": true, + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, + "node_modules/last-one-wins": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/last-one-wins/-/last-one-wins-1.0.4.tgz", + "integrity": "sha512-t+KLJFkHPQk8lfN6WBOiGkiUXoub+gnb2XTYI2P3aiISL+94xgZ1vgz1SXN/N4hthuOoLXarXfBZPUruyjQtfA==", + "dev": true, + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "optional": true, + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "dev": true, + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "node_modules/linkify-it": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", + "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", + "dev": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/load-ip-set": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/load-ip-set/-/load-ip-set-3.0.1.tgz", + "integrity": "sha512-ZFZt1g4Exq01SFtKjffqau+L4Qibt+51utymHHiWo8Iu/W7LYSqE7fiZ/iAZ6dIqbmeU6ICSIK02IizSScBkLQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "cross-fetch-ponyfill": "^1.0.1", + "ip-set": "^2.1.0", + "netmask": "^2.0.1", + "once": "^1.4.0", + "queue-microtask": "^1.2.3", + "split": "^1.0.1" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "dev": true, + "dependencies": { + "lie": "3.1.1" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "devOptional": true + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lru/-/lru-3.1.0.tgz", + "integrity": "sha512-5OUtoiVIGU4VXBOshidmtOsvBIvcQR6FD/RzWSvaeHyxCGB+PCUCu+52lqMfdc0h/2CLvHhZS4TwUmMQrrMbBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-memoizer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.2.0.tgz", + "integrity": "sha512-QfOZ6jNkxCcM/BkIPnFsqDhtrazLRsghi9mBwFAzol5GCvj4EkFT899Za3+QwikCg5sRX8JstioBDwOxEyzaNw==", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "~4.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==", + "dependencies": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + }, + "node_modules/lt_donthave": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/lt_donthave/-/lt_donthave-2.0.6.tgz", + "integrity": "sha512-ZVcaRbZpNB6ugwa5T9gUN0Jg9XGT9cyVjZJvdbN3V27rOQ170rEs//zaQXEQkTCBhh3i/JnCRF472KWHJu74Yg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "debug": "^4.2.0", + "unordered-array-remove": "^1.0.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/lt_donthave/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/lt_donthave/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-bytes.js": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz", + "integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==", + "license": "MIT" + }, + "node_modules/magnet-uri": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/magnet-uri/-/magnet-uri-7.0.7.tgz", + "integrity": "sha512-z/+dB2NQsXaDuxVBjoPLpZT8ePaacUmoontoFheRBl++nALHYs4qV9MmhTur9e4SaMbkCR/uPX43UMzEOoeyaw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "@thaunknown/thirty-two": "^1.0.5", + "bep53-range": "^2.0.0", + "uint8-util": "^2.2.5" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "optional": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "optional": true, + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/markdown-it/node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "optional": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "optional": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "optional": true + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mediasoup-client": { + "version": "3.18.1", + "resolved": "https://registry.npmjs.org/mediasoup-client/-/mediasoup-client-3.18.1.tgz", + "integrity": "sha512-RKo+hS3snKOnKRJr5la3aw5YKzUHKadsfFpOeNeOh2XCB+TbOLx751zSQErzfTGPQLYtcMZOQedRsZMA2uXd9A==", + "dev": true, + "license": "ISC", + "dependencies": { + "@types/debug": "^4.1.12", + "@types/events-alias": "npm:@types/events@^3.0.3", + "awaitqueue": "^3.3.0", + "debug": "^4.4.3", + "events-alias": "npm:events@^3.3.0", + "fake-mediastreamtrack": "^2.2.1", + "h264-profile-level-id": "^2.3.1", + "sdp-transform": "^3.0.0", + "supports-color": "^10.2.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mediasoup" + } + }, + "node_modules/mediasoup-client/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mediasoup-client/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mediasoup-client/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/memory-chunk-store": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/memory-chunk-store/-/memory-chunk-store-1.3.5.tgz", + "integrity": "sha512-E1Xc1U4ifk/FkC2ZsWhCaW1xg9HbE/OBmQTLe2Tr9c27YPSLbW7kw1cnb3kQWD1rDtErFJHa7mB9EVrs7aTx9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.3" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "devOptional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "devOptional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/nan": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", + "optional": true + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/napi-macros": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.2.2.tgz", + "integrity": "sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g==", + "dev": true, + "optional": true + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/node-abi": { + "version": "3.77.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.77.0.tgz", + "integrity": "sha512-DSmt0OEcLoK4i3NuscSbGjOf3bqiDEutejqENSplMSFA/gmB8mkED9G4pKWnPl7MDU4rSHebKPHeitpDfyH0cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-datachannel": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/node-datachannel/-/node-datachannel-0.12.0.tgz", + "integrity": "sha512-pZ9FsVZpHdUKqyWynuCc9IBLkZPJMpDzpNk4YNPCizbIXHYifpYeWqSF35REHGIWi9JMCf11QzapsyQGo/Y4Ig==", + "dev": true, + "hasInstallScript": true, + "license": "MPL 2.0", + "dependencies": { + "node-domexception": "^2.0.1", + "prebuild-install": "^7.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/node-datachannel/node_modules/node-domexception": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-2.0.2.tgz", + "integrity": "sha512-Qf9vHK9c5MGgUXj8SnucCIS4oEPuUstjRaMplLGeZpbWMfNV1rvEcXuwoXfN51dUfD1b4muPHPQtCx/5Dj/QAA==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "devOptional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.2.tgz", + "integrity": "sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.1.tgz", + "integrity": "sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==", + "dev": true, + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-ssh": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/node-ssh/-/node-ssh-13.2.1.tgz", + "integrity": "sha512-rfl4GWMygQfzlExPkQ2LWyya5n2jOBm5vhEnup+4mdw7tQhNpJWbP5ldr09Jfj93k5SfY5lxcn8od5qrQ/6mBg==", + "license": "MIT", + "dependencies": { + "is-stream": "^2.0.0", + "make-dir": "^3.1.0", + "sb-promise-queue": "^2.1.0", + "sb-scandir": "^3.1.0", + "shell-escape": "^0.2.0", + "ssh2": "^1.14.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "optional": true, + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "optional": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-torrent": { + "version": "11.0.18", + "resolved": "https://registry.npmjs.org/parse-torrent/-/parse-torrent-11.0.18.tgz", + "integrity": "sha512-C1igbmTrQQuKlspAfP1wcLaOPlvtu5qi4pMdPoCCfepHmxDOk8iArJ2J1yblLx11UefZJUaKEPSxIwMdG11SuA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "bencode": "^4.0.0", + "cross-fetch-ponyfill": "^1.0.3", + "get-stdin": "^9.0.0", + "magnet-uri": "^7.0.7", + "queue-microtask": "^1.2.3", + "uint8-util": "^2.2.5" + }, + "bin": { + "parse-torrent": "bin/cmd.js" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/piece-length": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/piece-length/-/piece-length-2.0.1.tgz", + "integrity": "sha512-dBILiDmm43y0JPISWEmVGKBETQjwJe6mSU9GND+P9KW0SJGUwoU/odyH1nbalOP9i8WSYuqf1lQnaj92Bhw+Ug==", + "dev": true, + "license": "MIT" + }, + "node_modules/pm2": { + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/pm2/-/pm2-6.0.14.tgz", + "integrity": "sha512-wX1FiFkzuT2H/UUEA8QNXDAA9MMHDsK/3UHj6Dkd5U7kxyigKDA5gyDw78ycTQZAuGCLWyUX5FiXEuVQWafukA==", + "license": "AGPL-3.0", + "dependencies": { + "@pm2/agent": "~2.1.1", + "@pm2/blessed": "0.1.81", + "@pm2/io": "~6.1.0", + "@pm2/js-api": "~0.8.0", + "@pm2/pm2-version-check": "^1.0.4", + "ansis": "4.0.0-node10", + "async": "3.2.6", + "chokidar": "3.6.0", + "cli-tableau": "2.0.1", + "commander": "2.15.1", + "croner": "4.1.97", + "dayjs": "1.11.15", + "debug": "4.4.3", + "enquirer": "2.3.6", + "eventemitter2": "5.0.1", + "fclone": "1.0.11", + "js-yaml": "4.1.1", + "mkdirp": "1.0.4", + "needle": "2.4.0", + "pidusage": "3.0.2", + "pm2-axon": "~4.0.1", + "pm2-axon-rpc": "~0.7.1", + "pm2-deploy": "~1.0.2", + "pm2-multimeter": "^0.1.2", + "promptly": "2.2.0", + "semver": "7.7.2", + "source-map-support": "0.5.21", + "sprintf-js": "1.1.2", + "vizion": "~2.2.1" + }, + "bin": { + "pm2": "bin/pm2", + "pm2-dev": "bin/pm2-dev", + "pm2-docker": "bin/pm2-docker", + "pm2-runtime": "bin/pm2-runtime" + }, + "engines": { + "node": ">=16.0.0" + }, + "optionalDependencies": { + "pm2-sysmonit": "^1.2.8" + } + }, + "node_modules/pm2/node_modules/@pm2/agent": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@pm2/agent/-/agent-2.1.1.tgz", + "integrity": "sha512-0V9ckHWd/HSC8BgAbZSoq8KXUG81X97nSkAxmhKDhmF8vanyaoc1YXwc2KVkbWz82Rg4gjd2n9qiT3i7bdvGrQ==", + "dependencies": { + "async": "~3.2.0", + "chalk": "~3.0.0", + "dayjs": "~1.8.24", + "debug": "~4.3.1", + "eventemitter2": "~5.0.1", + "fast-json-patch": "^3.1.0", + "fclone": "~1.0.11", + "pm2-axon": "~4.0.1", + "pm2-axon-rpc": "~0.7.0", + "proxy-agent": "~6.4.0", + "semver": "~7.5.0", + "ws": "~7.5.10" + } + }, + "node_modules/pm2/node_modules/@pm2/agent/node_modules/dayjs": { + "version": "1.8.36", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.36.tgz", + "integrity": "sha512-3VmRXEtw7RZKAf+4Tv1Ym9AGeo8r8+CjDi26x+7SYQil1UqtqdaokhzoEJohqlzt0m5kacJSDhJQkG/LWhpRBw==" + }, + "node_modules/pm2/node_modules/@pm2/agent/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/pm2/node_modules/@pm2/agent/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pm2/node_modules/@pm2/agent/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pm2/node_modules/@pm2/io": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@pm2/io/-/io-6.1.0.tgz", + "integrity": "sha512-IxHuYURa3+FQ6BKePlgChZkqABUKFYH6Bwbw7V/pWU1pP6iR1sCI26l7P9ThUEB385ruZn/tZS3CXDUF5IA1NQ==", + "dependencies": { + "async": "~2.6.1", + "debug": "~4.3.1", + "eventemitter2": "^6.3.1", + "require-in-the-middle": "^5.0.0", + "semver": "~7.5.4", + "shimmer": "^1.2.0", + "signal-exit": "^3.0.3", + "tslib": "1.9.3" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/pm2/node_modules/@pm2/io/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/pm2/node_modules/@pm2/io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/pm2/node_modules/@pm2/io/node_modules/eventemitter2": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", + "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==" + }, + "node_modules/pm2/node_modules/@pm2/io/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pm2/node_modules/@pm2/io/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pm2/node_modules/@pm2/js-api": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@pm2/js-api/-/js-api-0.8.0.tgz", + "integrity": "sha512-nmWzrA/BQZik3VBz+npRcNIu01kdBhWL0mxKmP1ciF/gTcujPTQqt027N9fc1pK9ERM8RipFhymw7RcmCyOEYA==", + "dependencies": { + "async": "^2.6.3", + "debug": "~4.3.1", + "eventemitter2": "^6.3.1", + "extrareqp2": "^1.0.0", + "ws": "^7.0.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/pm2/node_modules/@pm2/js-api/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/pm2/node_modules/@pm2/js-api/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/pm2/node_modules/@pm2/js-api/node_modules/eventemitter2": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", + "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==" + }, + "node_modules/pm2/node_modules/@pm2/pm2-version-check": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@pm2/pm2-version-check/-/pm2-version-check-1.0.4.tgz", + "integrity": "sha512-SXsM27SGH3yTWKc2fKR4SYNxsmnvuBQ9dd6QHtEWmiZ/VqaOYPAIlS8+vMcn27YLtAEBGvNRSh3TPNvtjZgfqA==", + "dependencies": { + "debug": "^4.3.1" + } + }, + "node_modules/pm2/node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==" + }, + "node_modules/pm2/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/pm2/node_modules/amp": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz", + "integrity": "sha512-OwIuC4yZaRogHKiuU5WlMR5Xk/jAcpPtawWL05Gj8Lvm2F6mwoJt4O/bHI+DHwG79vWd+8OFYM4/BzYqyRd3qw==" + }, + "node_modules/pm2/node_modules/amp-message": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/amp-message/-/amp-message-0.1.2.tgz", + "integrity": "sha512-JqutcFwoU1+jhv7ArgW38bqrE+LQdcRv4NxNw0mp0JHQyB6tXesWRjtYKlDgHRY2o3JE5UTaBGUK8kSWUdxWUg==", + "dependencies": { + "amp": "0.3.1" + } + }, + "node_modules/pm2/node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/pm2/node_modules/ansis": { + "version": "4.0.0-node10", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.0.0-node10.tgz", + "integrity": "sha512-BRrU0Bo1X9dFGw6KgGz6hWrqQuOlVEDOzkb0QSLZY9sXHqA7pNj7yHPVJRz7y/rj4EOJ3d/D5uxH+ee9leYgsg==", + "engines": { + "node": ">=10" + } + }, + "node_modules/pm2/node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/pm2/node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pm2/node_modules/ast-types/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/pm2/node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, + "node_modules/pm2/node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pm2/node_modules/bodec": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bodec/-/bodec-0.1.0.tgz", + "integrity": "sha512-Ylo+MAo5BDUq1KA3f3R/MFhh+g8cnHmo8bz3YPGhI1znrMaf77ol1sfvYJzsw3nTE+Y2GryfDxBaR+AqpAkEHQ==" + }, + "node_modules/pm2/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pm2/node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/pm2/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pm2/node_modules/charm": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/charm/-/charm-0.1.2.tgz", + "integrity": "sha512-syedaZ9cPe7r3hoQA9twWYKu5AIyCswN5+szkmPBe9ccdLrj4bYaCnLVPTLd2kgVRc7+zoX4tyPgRnFKCj5YjQ==" + }, + "node_modules/pm2/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "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" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/pm2/node_modules/cli-tableau": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cli-tableau/-/cli-tableau-2.0.1.tgz", + "integrity": "sha512-he+WTicka9cl0Fg/y+YyxcN6/bfQ/1O3QmgxRXDhABKqLzvoOSM4fMzp39uMyLBulAFuywD2N7UaoQE7WaADxQ==", + "dependencies": { + "chalk": "3.0.0" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/pm2/node_modules/commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" + }, + "node_modules/pm2/node_modules/croner": { + "version": "4.1.97", + "resolved": "https://registry.npmjs.org/croner/-/croner-4.1.97.tgz", + "integrity": "sha512-/f6gpQuxDaqXu+1kwQYSckUglPaOrHdbIlBAu0YuW8/Cdb45XwXYNUBXg3r/9Mo6n540Kn/smKcZWko5x99KrQ==" + }, + "node_modules/pm2/node_modules/culvert": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/culvert/-/culvert-0.1.2.tgz", + "integrity": "sha512-yi1x3EAWKjQTreYWeSd98431AV+IEE0qoDyOoaHJ7KJ21gv6HtBXHVLX74opVSGqcR8/AbjJBHAHpcOy2bj5Gg==" + }, + "node_modules/pm2/node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/pm2/node_modules/dayjs": { + "version": "1.11.15", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.15.tgz", + "integrity": "sha512-MC+DfnSWiM9APs7fpiurHGCoeIx0Gdl6QZBy+5lu8MbYKN5FZEXqOgrundfibdfhGZ15o9hzmZ2xJjZnbvgKXQ==" + }, + "node_modules/pm2/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/pm2/node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pm2/node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/pm2/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pm2/node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/pm2/node_modules/eventemitter2": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz", + "integrity": "sha512-5EM1GHXycJBS6mauYAbVKT1cVs7POKWb2NXD4Vyt8dDqeZa7LaDK1/sjtL+Zb0lzTpSNil4596Dyu97hz37QLg==" + }, + "node_modules/pm2/node_modules/extrareqp2": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/extrareqp2/-/extrareqp2-1.0.0.tgz", + "integrity": "sha512-Gum0g1QYb6wpPJCVypWP3bbIuaibcFiJcpuPM10YSXp/tzqi84x9PJageob+eN4xVRIOto4wjSGNLyMD54D2xA==", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/pm2/node_modules/fast-json-patch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==" + }, + "node_modules/pm2/node_modules/fclone": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fclone/-/fclone-1.0.11.tgz", + "integrity": "sha512-GDqVQezKzRABdeqflsgMr7ktzgF9CyS+p2oe0jJqUY6izSSbhPIQJDpoU4PtGcD7VPM9xh/dVrTu6z1nwgmEGw==" + }, + "node_modules/pm2/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pm2/node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pm2/node_modules/git-node-fs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/git-node-fs/-/git-node-fs-1.0.0.tgz", + "integrity": "sha512-bLQypt14llVXBg0S0u8q8HmU7g9p3ysH+NvVlae5vILuUvs759665HvmR5+wb04KjHyjFcDRxdYb4kyNnluMUQ==" + }, + "node_modules/pm2/node_modules/git-sha1": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/git-sha1/-/git-sha1-0.1.2.tgz", + "integrity": "sha512-2e/nZezdVlyCopOCYHeW0onkbZg7xP1Ad6pndPy1rCygeRykefUS6r7oA5cJRGEFvseiaz5a/qUHFVX1dd6Isg==" + }, + "node_modules/pm2/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pm2/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pm2/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pm2/node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pm2/node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/pm2/node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pm2/node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pm2/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/pm2/node_modules/js-git": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/js-git/-/js-git-0.7.8.tgz", + "integrity": "sha512-+E5ZH/HeRnoc/LW0AmAyhU+mNcWBzAKE+30+IDMLSLbbK+Tdt02AdkOKq9u15rlJsDEGFqtgckc8ZM59LhhiUA==", + "dependencies": { + "bodec": "^0.1.0", + "culvert": "^0.1.2", + "git-sha1": "^0.1.2", + "pako": "^0.2.5" + } + }, + "node_modules/pm2/node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "optional": true + }, + "node_modules/pm2/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/pm2/node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==" + }, + "node_modules/pm2/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/pm2/node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" + }, + "node_modules/pm2/node_modules/needle": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", + "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/pm2/node_modules/needle/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/pm2/node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pm2/node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pm2/node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pm2/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==" + }, + "node_modules/pm2/node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/pm2/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pm2/node_modules/pidusage": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-3.0.2.tgz", + "integrity": "sha512-g0VU+y08pKw5M8EZ2rIGiEBaB8wrQMjYGFfW2QVIfyT8V+fq8YFLkvlz4bz5ljvFDJYNFCWT3PWqcRr2FKO81w==", + "dependencies": { + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pm2/node_modules/pm2-axon": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pm2-axon/-/pm2-axon-4.0.1.tgz", + "integrity": "sha512-kES/PeSLS8orT8dR5jMlNl+Yu4Ty3nbvZRmaAtROuVm9nYYGiaoXqqKQqQYzWQzMYWUKHMQTvBlirjE5GIIxqg==", + "dependencies": { + "amp": "~0.3.1", + "amp-message": "~0.1.1", + "debug": "^4.3.1", + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=5" + } + }, + "node_modules/pm2/node_modules/pm2-axon-rpc": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/pm2-axon-rpc/-/pm2-axon-rpc-0.7.1.tgz", + "integrity": "sha512-FbLvW60w+vEyvMjP/xom2UPhUN/2bVpdtLfKJeYM3gwzYhoTEEChCOICfFzxkxuoEleOlnpjie+n1nue91bDQw==", + "dependencies": { + "debug": "^4.3.1" + }, + "engines": { + "node": ">=5" + } + }, + "node_modules/pm2/node_modules/pm2-deploy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pm2-deploy/-/pm2-deploy-1.0.2.tgz", + "integrity": "sha512-YJx6RXKrVrWaphEYf++EdOOx9EH18vM8RSZN/P1Y+NokTKqYAca/ejXwVLyiEpNju4HPZEk3Y2uZouwMqUlcgg==", + "dependencies": { + "run-series": "^1.1.8", + "tv4": "^1.3.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pm2/node_modules/pm2-multimeter": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/pm2-multimeter/-/pm2-multimeter-0.1.2.tgz", + "integrity": "sha512-S+wT6XfyKfd7SJIBqRgOctGxaBzUOmVQzTAS+cg04TsEUObJVreha7lvCfX8zzGVr871XwCSnHUU7DQQ5xEsfA==", + "dependencies": { + "charm": "~0.1.1" + } + }, + "node_modules/pm2/node_modules/pm2-sysmonit": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/pm2-sysmonit/-/pm2-sysmonit-1.2.8.tgz", + "integrity": "sha512-ACOhlONEXdCTVwKieBIQLSi2tQZ8eKinhcr9JpZSUAL8Qy0ajIgRtsLxG/lwPOW3JEKqPyw/UaHmTWhUzpP4kA==", + "optional": true, + "dependencies": { + "async": "^3.2.0", + "debug": "^4.3.1", + "pidusage": "^2.0.21", + "systeminformation": "^5.7", + "tx2": "~1.0.4" + } + }, + "node_modules/pm2/node_modules/pm2-sysmonit/node_modules/pidusage": { + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-2.0.21.tgz", + "integrity": "sha512-cv3xAQos+pugVX+BfXpHsbyz/dLzX+lr44zNMsYiGxUw+kV5sgQCIcLd1z+0vq+KyC7dJ+/ts2PsfgWfSC3WXA==", + "optional": true, + "dependencies": { + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pm2/node_modules/promptly": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/promptly/-/promptly-2.2.0.tgz", + "integrity": "sha512-aC9j+BZsRSSzEsXBNBwDnAxujdx19HycZoKgRgzWnS8eOHg1asuf9heuLprfbe739zY3IdUQx+Egv6Jn135WHA==", + "dependencies": { + "read": "^1.0.4" + } + }, + "node_modules/pm2/node_modules/proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pm2/node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/pm2/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/pm2/node_modules/require-in-the-middle": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-5.2.0.tgz", + "integrity": "sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg==", + "dependencies": { + "debug": "^4.1.1", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pm2/node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/pm2/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pm2/node_modules/shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" + }, + "node_modules/pm2/node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pm2/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/pm2/node_modules/sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==" + }, + "node_modules/pm2/node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/pm2/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/pm2/node_modules/tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" + }, + "node_modules/pm2/node_modules/tv4": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz", + "integrity": "sha512-afizzfpJgvPr+eDkREK4MxJ/+r8nEEHcmitwgnPUqpaP+FpwQyadnxNoSACbgc/b1LsZYtODGoPiFxQrgJgjvw==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pm2/node_modules/tx2": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tx2/-/tx2-1.0.5.tgz", + "integrity": "sha512-sJ24w0y03Md/bxzK4FU8J8JveYYUbSs2FViLJ2D/8bytSiyPRbuE3DyL/9UKYXTZlV3yXq0L8GLlhobTnekCVg==", + "optional": true, + "dependencies": { + "json-stringify-safe": "^5.0.1" + } + }, + "node_modules/pm2/node_modules/vizion": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vizion/-/vizion-2.2.1.tgz", + "integrity": "sha512-sfAcO2yeSU0CSPFI/DmZp3FsFE9T+8913nv1xWBOyzODv13fwkn6Vl7HqxGpkr9F608M+8SuFId3s+BlZqfXww==", + "dependencies": { + "async": "^2.6.3", + "git-node-fs": "^1.0.0", + "ini": "^1.3.5", + "js-git": "^0.7.8" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/pm2/node_modules/vizion/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/pm2/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "optional": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/proto3-json-serializer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.1.tgz", + "integrity": "sha512-AwAuY4g9nxx0u52DnSMkqqgyLHaW/XaPLtaAo3y/ZCfeaQB/g4YDH4kb8Wc/mWzWvu0YjOznVnfn373MVZZrgw==", + "optional": true, + "dependencies": { + "protobufjs": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proto3-json-serializer/node_modules/protobufjs": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz", + "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/protobufjs": { + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, + "node_modules/protobufjs/node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true, + "license": "MIT" + }, + "node_modules/random-access-file": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/random-access-file/-/random-access-file-4.1.2.tgz", + "integrity": "sha512-GQM6R78DceZDcQod8KxlDFwXIiUvlvuy1EOzxTDsjuDjW5NlnlZi0MOk6iI4itAj/2vcvdqcEExYbVpC/dJcEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0", + "random-access-storage": "^3.0.0" + }, + "optionalDependencies": { + "fs-native-extensions": "^1.3.1" + } + }, + "node_modules/random-access-storage": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/random-access-storage/-/random-access-storage-3.0.2.tgz", + "integrity": "sha512-Es9maUyWdJXWKckKy9s1+vT+DEgAt+PBb9lxPaake/0EDUsHehloKGv9v1zimS2V3gpFAcQXubvc1Rgci2sDPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bare-events": "^2.2.0", + "queue-tick": "^1.0.0" + } + }, + "node_modules/random-iterate": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/random-iterate/-/random-iterate-1.0.1.tgz", + "integrity": "sha512-Jdsdnezu913Ot8qgKgSgs63XkAjEsnMcS1z+cC6D6TNXsUXsMxy0RpclF2pzGZTEiTXL9BiArdGTEexcv4nqcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc4": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/rc4/-/rc4-0.1.5.tgz", + "integrity": "sha512-xdDTNV90z5x5u25Oc871Xnvu7yAr4tV7Eluh0VSvrhUkry39q1k+zkz7xroqHbRq+8PiazySHJPArqifUvz9VA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-colorful": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz", + "integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==", + "dev": true, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/react-countup": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/react-countup/-/react-countup-6.5.3.tgz", + "integrity": "sha512-udnqVQitxC7QWADSPDOxVWULkLvKUWrDapn5i53HE4DPRVgs+Y5rr4bo25qEl8jSh+0l2cToJgGMx+clxPM3+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "countup.js": "^2.8.0" + }, + "peerDependencies": { + "react": ">= 16.3.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-linkify": { + "version": "1.0.0-alpha", + "resolved": "https://registry.npmjs.org/react-linkify/-/react-linkify-1.0.0-alpha.tgz", + "integrity": "sha512-7gcIUvJkAXXttt1fmBK9cwn+1jTa4hbKLGCZ9J1U6EOkyb2/+LKL1Z28d9rtDLMnpvImlNlLPdTPooorl5cpmg==", + "dev": true, + "dependencies": { + "linkify-it": "^2.0.3", + "tlds": "^1.199.0" + } + }, + "node_modules/react-number-format": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.4.tgz", + "integrity": "sha512-wOmoNZoOpvMminhifQYiYSTCLUDOiUbBunrMrMjA+dV52sY+vck1S4UhR6PkgnoCquvvMSeJjErXZ4qSaWCliA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-onclickoutside": { + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.2.tgz", + "integrity": "sha512-h6Hbf1c8b7tIYY4u90mDdBLY4+AGQVMFtIE89HgC0DtVCh/JfKl477gYqUtGLmjZBKK3MJxomP/lFiLbz4sq9A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md" + }, + "peerDependencies": { + "react": "^15.5.x || ^16.x || ^17.x || ^18.x", + "react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", + "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/react-router-dom": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", + "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.2", + "react-router": "5.3.4", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/react-router/node_modules/path-to-regexp": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/react-router/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/react-secure-link": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/react-secure-link/-/react-secure-link-3.2.0.tgz", + "integrity": "sha512-ypJT9C7CyOR2noDCSCGjzzVgF4CRLqxzP5oWj+/Qy6QOETCgKTIIh/jmEYlVg+8XtuxM2tBbw6EeQz98TRbCOQ==", + "dev": true, + "peerDependencies": { + "react": ">=16" + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-textarea-autosize": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz", + "integrity": "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "use-composed-ref": "^1.3.0", + "use-latest": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "devOptional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "dev": true, + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/record-cache": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/record-cache/-/record-cache-1.2.0.tgz", + "integrity": "sha512-kyy3HWCez2WrotaL3O4fTn0rsIdfRKOdQQcEJ9KpvmKmbffKVvwsloX063EgRUlpJIXHiDQFhJcTbZequ2uTZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.3.1" + } + }, + "node_modules/record-cache/node_modules/b4a": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.1.tgz", + "integrity": "sha512-ZovbrBV0g6JxK5cGUF1Suby1vLfKjv4RWi8IxoaO/Mon8BDD9I21RxjHFtgQ+kskJqLAVyQZly3uMBui+vhc8Q==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/require-addon": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.1.0.tgz", + "integrity": "sha512-KbXAD5q2+v1GJnkzd8zzbOxchTkStSyJZ9QwoCq3QwEXAaIlG3wDYRZGzVD357jmwaGY7hr5VaoEAL0BkF0Kvg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-addon-resolve": "^1.3.0", + "bare-url": "^2.1.0" + }, + "engines": { + "bare": ">=1.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "optional": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==", + "dev": true + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz", + "integrity": "sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ==", + "optional": true, + "dependencies": { + "debug": "^4.1.1", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/retry-request/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/retry-request/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/run-parallel-limit": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/run-parallel-limit/-/run-parallel-limit-1.1.0.tgz", + "integrity": "sha512-jJA7irRNM91jaKc3Hcl1npHsFLOXOoTkPCUL1JEa1R82O2miplXXRaGdjW/KM/98YQWDhJLiSs793CnXfblJUw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/run-series": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/run-series/-/run-series-1.1.9.tgz", + "integrity": "sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" + }, + "node_modules/sb-promise-queue": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sb-promise-queue/-/sb-promise-queue-2.1.0.tgz", + "integrity": "sha512-zwq4YuP1FQFkGx2Q7GIkZYZ6PqWpV+bg0nIO1sJhWOyGyhqbj0MsTvK6lCFo5TQwX5pZr6SCQ75e8PCDCuNvkg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/sb-scandir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/sb-scandir/-/sb-scandir-3.1.0.tgz", + "integrity": "sha512-70BVm2xz9jn94zSQdpvYrEG101/UV9TVGcfWr9T5iob3QhCK4lYXeculfBqPGFv3XTeKgx4dpWyYIDeZUqo4kg==", + "dependencies": { + "sb-promise-queue": "^2.1.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/sdp-transform": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-3.0.0.tgz", + "integrity": "sha512-gfYVRGxjHkGF2NPeUWHw5u6T/KGFtS5/drPms73gaSuMaVHKCY3lpLnGDfswVQO0kddeePoti09AwhYP4zA8dQ==", + "dev": true, + "license": "MIT", + "bin": { + "sdp-verify": "checker.js" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/send/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-escape": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/shell-escape/-/shell-escape-0.2.0.tgz", + "integrity": "sha512-uRRBT2MfEOyxuECseCZd28jC1AJ8hmqqneWQ4VWUTgCAFvb3wKU1jLqj6egC4Exrr88ogg3dp+zroH4wJuaXzw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-client/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speed-limiter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/speed-limiter/-/speed-limiter-1.0.2.tgz", + "integrity": "sha512-Ax+TbUOho84bWUc3AKqWtkIvAIVws7d6QI4oJkgH4yQ5Yil+lR3vjd/7qd51dHKGzS5bFxg0++QwyNRN7s6rZA==", + "dev": true, + "dependencies": { + "limiter": "^1.1.5", + "streamx": "^2.10.3" + } + }, + "node_modules/split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/srt-webvtt": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/srt-webvtt/-/srt-webvtt-2.0.0.tgz", + "integrity": "sha512-G2Z7/Jf2NRKrmLYNSIhSYZZYE6OFlKXFp9Au2/zJBKgrioUzmrAys1x7GT01dwl6d2sEnqr5uahEIOd0JW/Rbw==", + "dev": true + }, + "node_modules/ssh2": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.15.0.tgz", + "integrity": "sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw==", + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.9", + "nan": "^2.18.0" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "optional": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "optional": true + }, + "node_modules/streamx": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", + "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "devOptional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string2compact": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string2compact/-/string2compact-2.0.1.tgz", + "integrity": "sha512-Bm/T8lHMTRXw+u83LE+OW7fXmC/wM+Mbccfdo533ajSBNxddDHlRrvxE49NdciGHgXkUQM5WYskJ7uTkbBUI0A==", + "dev": true, + "dependencies": { + "addr-to-ip-port": "^2.0.0", + "ipaddr.js": "^2.0.0" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/string2compact/node_modules/ipaddr.js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", + "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stripe": { + "version": "8.222.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-8.222.0.tgz", + "integrity": "sha512-hrA79fjmN2Eb6K3kxkDzU4ODeVGGjXQsuVaAPSUro6I9MM3X+BvIsVqdphm3BXWfimAGFvUqWtPtHy25mICY1w==", + "dependencies": { + "@types/node": ">=8.1.0", + "qs": "^6.10.3" + }, + "engines": { + "node": "^8.1 || >=10.*" + } + }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "optional": true + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "optional": true + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/systeminformation": { + "version": "5.31.3", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.3.tgz", + "integrity": "sha512-vX0eeI7oGIr79NLiJRWnK8SyxDjyiNOEanaQnHRNyb5ep8QcpD8QMDvrukdrxV4pV4AKjwUDfaypXnWHMC/65A==", + "license": "MIT", + "optional": true, + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, + "node_modules/tabbable": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz", + "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/teeny-request": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-8.0.3.tgz", + "integrity": "sha512-jJZpA5He2y52yUhA7pyAGZlgQpcB+xLjcN0eUFxr9c8hP/H7uOXbBNVo/O0C/xVfJLJs680jvkFgVJEEvk9+ww==", + "optional": true, + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-decoder/node_modules/b4a": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.1.tgz", + "integrity": "sha512-ZovbrBV0g6JxK5cGUF1Suby1vLfKjv4RWi8IxoaO/Mon8BDD9I21RxjHFtgQ+kskJqLAVyQZly3uMBui+vhc8Q==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/text-decoding": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-decoding/-/text-decoding-1.0.0.tgz", + "integrity": "sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==" + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/throughput": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/throughput/-/throughput-1.0.2.tgz", + "integrity": "sha512-jvK1ZXuhsggjb3qYQjMiU/AVYYiTeqT5thWvYR2yuy2LGM84P5MSSyAinwHahGsdBYKR9m9HncVR/3f3nFKkxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/timeout-refresh": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/timeout-refresh/-/timeout-refresh-1.0.3.tgz", + "integrity": "sha512-Mz0CX4vBGM5lj8ttbIFt7o4ZMxk/9rgudJRh76EvB7xXZMur7T/cjRiH2w4Fmkq0zxf2QpM8IFvOSRn8FEu3gA==", + "dev": true, + "optional": true + }, + "node_modules/tiny-invariant": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", + "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==", + "dev": true + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "dev": true + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tlds": { + "version": "1.245.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.245.0.tgz", + "integrity": "sha512-fbSQFQr8f41/e9q9IoKKVv7CUFvvE3TVJo7m0JQJZcf7jOOXRRf9DIP4Uf04aovYGKG686OvtV4ZrNY1bOz3aA==", + "dev": true, + "bin": { + "tlds": "bin.js" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/torrent-discovery": { + "version": "11.0.19", + "resolved": "https://registry.npmjs.org/torrent-discovery/-/torrent-discovery-11.0.19.tgz", + "integrity": "sha512-BLhdj7o0px+u72UuhJmq6CB0LBkZOa1nwgbd5ktyTELJlvcRL8EoxSSmSpzMOIScLGgslh1uLaAy/POhLpagtg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "bittorrent-dht": "^11.0.11", + "bittorrent-lsd": "^2.0.0", + "bittorrent-tracker": "^11.2.2", + "debug": "^4.4.3", + "run-parallel": "^1.2.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/torrent-discovery/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/torrent-discovery/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/torrent-piece": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/torrent-piece/-/torrent-piece-3.0.2.tgz", + "integrity": "sha512-K1A5tZ3BolFrUtnBpk9iDg8av1na0OgQ7E0IlA9tj0bcsPhLhzvln+oMtMmtkqAwmUsbNCilRm2ymUdZg0rVbQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "uint8-util": "^2.1.9" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" + }, + "node_modules/twitch-m3u8": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/twitch-m3u8/-/twitch-m3u8-1.1.5.tgz", + "integrity": "sha512-o037ePS9fwfuOmBe/ftji/e3cqzWq50AUvuIHJZ5E0pV73UMjn3BAm00aZUYVrt3Ns6k08HvhVjAIya/WYcGRg==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "optional": true, + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ua-parser-js": { + "version": "1.0.37", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz", + "integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "engines": { + "node": "*" + } + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "devOptional": true + }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uint8-util": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/uint8-util/-/uint8-util-2.2.5.tgz", + "integrity": "sha512-/QxVQD7CttWpVUKVPz9znO+3Dd4BdTSnFQ7pv/4drVhC9m4BaL2LFHTkJn6EsYoxT79VDq/2Gg8L0H22PrzyMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "license": "MIT", + "optional": true + }, + "node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/unordered-array-remove": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unordered-array-remove/-/unordered-array-remove-1.0.2.tgz", + "integrity": "sha512-45YsfD6svkgaCBNyvD+dFHm4qFX9g3wRSIVgWVPtm2OCnphvPxzJoe20ATsiNpNJrmzHifnxm+BN5F7gFT/4gw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unordered-set": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unordered-set/-/unordered-set-2.0.1.tgz", + "integrity": "sha512-eUmNTPzdx+q/WvOHW0bgGYLWvWHNT3PTKEQLg0MAQhc0AHASHVHoP/9YytYd4RBVariqno/mEUhVZN98CmD7bg==", + "dev": true, + "optional": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-composed-ref": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.4.0.tgz", + "integrity": "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", + "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-latest": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.3.0.tgz", + "integrity": "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "use-isomorphic-layout-effect": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/ut_metadata": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/ut_metadata/-/ut_metadata-4.0.3.tgz", + "integrity": "sha512-2tovup0VDYpT8t8+EhhhKBmbgIyiYyJQZ+Hf+/61+SvjuRS2MEeA5CiSARP4q+9/83Wu09OsGrUre/Zv6OI5NA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "bencode": "^4.0.0", + "bitfield": "^4.0.0", + "debug": "^4.2.0", + "uint8-util": "^2.1.3" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/ut_metadata/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ut_metadata/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/ut_pex": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/ut_pex/-/ut_pex-4.0.4.tgz", + "integrity": "sha512-isVTbp2TKGoMOu+4Zh/i6ijpYr0VG83xjRPgCXaUjKzgXXndjCMWg32/9kZjubD+kxEXcmXMkoS8IttS9FZE8g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "bencode": "^4.0.0", + "compact2string": "^1.4.1", + "string2compact": "^2.0.1" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "devOptional": true + }, + "node_modules/utp-native": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/utp-native/-/utp-native-2.5.3.tgz", + "integrity": "sha512-sWTrWYXPhhWJh+cS2baPzhaZc89zwlWCfwSthUjGhLkZztyPhcQllo+XVVCbNGi7dhyRlxkWxN4NKU6FbA9Y8w==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "napi-macros": "^2.0.0", + "node-gyp-build": "^4.2.0", + "readable-stream": "^3.0.2", + "timeout-refresh": "^1.0.0", + "unordered-set": "^2.0.1" + }, + "bin": { + "ucat": "ucat.js" + }, + "engines": { + "node": ">=8.12" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/value-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==", + "dev": true + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/victory-vendor": { + "version": "36.6.12", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.6.12.tgz", + "integrity": "sha512-pJrTkNHln+D83vDCCSUf0ZfxBvIaVrFHmrBOsnnLAbdqfudRACAj51He2zU94/IWq9464oTADcPVkmWAfNMwgA==", + "dev": true, + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/victory-vendor/node_modules/@types/d3-shape": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.5.tgz", + "integrity": "sha512-dfEWpZJ1Pdg8meLlICX1M3WBIpxnaH2eQV2eY43Y5ysRJOTAV9f3/R++lgJKFstfrEOE2zdJ0sv5qwr2Bkic6Q==", + "dev": true, + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/vite": { + "version": "7.2.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz", + "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/webrtc-polyfill": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/webrtc-polyfill/-/webrtc-polyfill-1.1.10.tgz", + "integrity": "sha512-sOn0bj3/noUdzQX7rvk0jFbBurqWDGGo2ipl+WfgoOe/x3cxbGLk/ZUY+WHCISSlLaIeBumi1X3wxQZnUESExQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-datachannel": "^v0.12.0", + "node-domexception": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/webtorrent": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/webtorrent/-/webtorrent-2.8.5.tgz", + "integrity": "sha512-oIjpuBrypApJ+RCZ8RRaHEncVSkt2cd25/I4Trb2sk9nlaEF92Dg1u8BCwqA4eJR7wIZQM95GyO7Wo4QTbrUUA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "@silentbot1/nat-api": "^0.4.9", + "@thaunknown/simple-peer": "^10.0.11", + "@webtorrent/http-node": "^1.3.0", + "addr-to-ip-port": "^2.0.0", + "bitfield": "^4.2.0", + "bittorrent-dht": "^11.0.10", + "bittorrent-protocol": "^4.1.20", + "cache-chunk-store": "^3.2.2", + "chunk-store-iterator": "^1.0.4", + "cpus": "^1.0.3", + "create-torrent": "^6.1.0", + "cross-fetch-ponyfill": "^1.0.3", + "debug": "^4.4.1", + "escape-html": "^1.0.3", + "fs-chunk-store": "^5.0.0", + "fsa-chunk-store": "^1.3.0", + "immediate-chunk-store": "^2.2.0", + "join-async-iterator": "^1.1.1", + "load-ip-set": "^3.0.1", + "lt_donthave": "^2.0.5", + "memory-chunk-store": "^1.3.5", + "mime": "^3.0.0", + "once": "^1.4.0", + "parse-torrent": "^11.0.18", + "pump": "^3.0.2", + "queue-microtask": "^1.2.3", + "random-iterate": "^1.0.1", + "range-parser": "^1.2.1", + "run-parallel": "^1.2.0", + "run-parallel-limit": "^1.1.0", + "speed-limiter": "^1.0.2", + "streamx": "2.22.1", + "throughput": "^1.0.2", + "torrent-discovery": "^11.0.17", + "torrent-piece": "^3.0.2", + "uint8-util": "^2.2.5", + "unordered-array-remove": "^1.0.2", + "ut_metadata": "^4.0.3", + "ut_pex": "^4.0.4" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "utp-native": "^2.5.3" + } + }, + "node_modules/webtorrent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/webtorrent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-runtime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.3.2.tgz", + "integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "optional": true + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5462e54 --- /dev/null +++ b/package.json @@ -0,0 +1,92 @@ +{ + "name": "watchparty", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "start": "node server/server.ts", + "ui": "vite --host", + "dev": "node --watch-path . server/server.ts", + "build": "npm run buildReact && npm run typecheckServer", + "buildReact": "vite build && npm run typecheck", + "pm2": "pm2 start server/server.ts", + "deploy": "git fetch && git reset --hard origin/release && pm2 flush && pm2 delete all && pm2 start server/ecosystem.config.js", + "prettier": "npx prettier --write .", + "typecheckServer": "tsc --project server/tsconfig.json", + "typecheck": "tsc --project src/tsconfig.json --noEmit", + "ncu": "ncu --semverLevel major -u", + "updateSnapshot": "curl -X POST localhost:3100/updateSnapshot -H 'Content-Type: application/json' -d '{\"provider\":\"Hetzner\",\"region\":\"US\"}'", + "testvBrowser": "docker run -d --rm --name=vbrowser --log-opt max-size=1g --net=host --shm-size=1g --cap-add=\"SYS_ADMIN\" -e DISPLAY=\":99.0\" -e NEKO_PASSWORD=user -e NEKO_PASSWORD_ADMIN=admin -e NEKO_BIND=\":5100\" -e NEKO_EPR=\":59000-59100\" -e NEKO_H264=\"1\" howardc93/vbrowser", + "testvlc": "docker run -d --rm --name=vlc --log-opt max-size=1g --net=host -e VLC_MEDIA=\"https://github.com/ietf-wg-cellar/matroska-test-files/raw/master/test_files/test1.mkv\" -e NEKO_PASSWORD=user -e NEKO_PASSWORD_ADMIN=admin -e NEKO_BIND=\":5100\" -e NEKO_EPR=\":59000-59100\" -e NEKO_H264=\"1\" m1k1o/neko:vlc", + "analyze": "npx source-map-explorer 'build/assets/*.js' --html result.html --no-border-checks" + }, + "dependencies": { + "@googleapis/youtube": "^8.0.2", + "axios": "^1.13.2", + "body-parser": "^1.20.4", + "compression": "^1.8.1", + "cors": "^2.8.5", + "discord.js": "^14.25.1", + "express": "^5.2.1", + "firebase-admin": "^11.11.1", + "ioredis": "^5.8.2", + "node-ssh": "^13.2.1", + "pg": "^8.16.3", + "pm2": "^6.0.14", + "socket.io": "^4.8.1", + "stripe": "^8.222.0" + }, + "devDependencies": { + "@emoji-mart/data": "^1.2.1", + "@emoji-mart/react": "^1.1.1", + "@mantine/core": "^8.3.10", + "@stripe/stripe-js": "^1.54.2", + "@tabler/icons-react": "^3.35.0", + "@types/compression": "^1.8.1", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/node": "^25.0.1", + "@types/pg": "^8.16.0", + "@types/react": "^18.3.27", + "@types/react-dom": "^18.3.7", + "@types/react-onclickoutside": "^6.7.10", + "@types/react-router-dom": "^5.3.3", + "@types/react-transition-group": "^4.4.12", + "@types/webtorrent": "^0.110.1", + "@types/youtube": "^0.1.2", + "can-autoplay": "^3.0.2", + "dashjs": "^5.1.0", + "eventemitter3": "^4.0.7", + "firebase": "^9.23.0", + "hls.js": "^1.6.15", + "mediasoup-client": "^3.18.1", + "react": "^18.3.1", + "react-colorful": "^5.6.1", + "react-countup": "^6.5.3", + "react-dom": "^18.3.1", + "react-linkify": "^1.0.0-alpha", + "react-onclickoutside": "^6.13.2", + "react-router-dom": "^5.3.4", + "react-secure-link": "^3.2.0", + "react-transition-group": "^4.4.5", + "recharts": "^2.15.4", + "socket.io-client": "^4.8.1", + "srt-webvtt": "^2.0.0", + "twitch-m3u8": "^1.1.5", + "typescript": "^5.9.3", + "vite": "^7.2.7", + "webtorrent": "^2.8.5" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/public/clearly.mp3 b/public/clearly.mp3 new file mode 100644 index 0000000..feb29fb Binary files /dev/null and b/public/clearly.mp3 differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..90dda5e Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/flag-european-union.png b/public/flag-european-union.png new file mode 100644 index 0000000..45cd500 Binary files /dev/null and b/public/flag-european-union.png differ diff --git a/public/flag-united-states.png b/public/flag-united-states.png new file mode 100644 index 0000000..b9d5da2 Binary files /dev/null and b/public/flag-united-states.png differ diff --git a/public/logo192.png b/public/logo192.png new file mode 100644 index 0000000..61372b3 Binary files /dev/null and b/public/logo192.png differ diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/public/screenshot14.png b/public/screenshot14.png new file mode 100644 index 0000000..04c5bcd Binary files /dev/null and b/public/screenshot14.png differ diff --git a/public/screenshot18.png b/public/screenshot18.png new file mode 100644 index 0000000..71e19a0 Binary files /dev/null and b/public/screenshot18.png differ diff --git a/public/screenshot4.png b/public/screenshot4.png new file mode 100644 index 0000000..ed9fdb6 Binary files /dev/null and b/public/screenshot4.png differ diff --git a/public/screenshot5.png b/public/screenshot5.png new file mode 100644 index 0000000..b13ceb5 Binary files /dev/null and b/public/screenshot5.png differ diff --git a/public/screenshot7.png b/public/screenshot7.png new file mode 100644 index 0000000..3c9a04c Binary files /dev/null and b/public/screenshot7.png differ diff --git a/public/screenshot_full.png b/public/screenshot_full.png new file mode 100644 index 0000000..f9c4379 Binary files /dev/null and b/public/screenshot_full.png differ diff --git a/public/sw.min.js b/public/sw.min.js new file mode 100644 index 0000000..8d5874c --- /dev/null +++ b/public/sw.min.js @@ -0,0 +1,82 @@ +(() => { + "use strict"; + let e = !1; + (self.addEventListener("install", () => { + self.skipWaiting(); + }), + self.addEventListener("fetch", (s) => { + const t = ((s) => { + const { url: t } = s.request; + return t.includes(self.registration.scope + "webtorrent/") + ? t.includes(self.registration.scope + "webtorrent/keepalive/") + ? new Response() + : t.includes(self.registration.scope + "webtorrent/cancel/") + ? new Response( + new ReadableStream({ + cancel() { + e = !0; + }, + }), + ) + : (async function ({ request: s }) { + const { url: t, method: n, headers: o, destination: a } = s, + l = await clients.matchAll({ + type: "window", + includeUncontrolled: !0, + }), + [r, i] = await new Promise((e) => { + for (const s of l) { + const l = new MessageChannel(), + { port1: r, port2: i } = l; + ((r.onmessage = ({ data: s }) => { + e([s, r]); + }), + s.postMessage( + { + url: t, + method: n, + headers: Object.fromEntries(o.entries()), + scope: self.registration.scope, + destination: a, + type: "webtorrent", + }, + [i], + )); + } + }); + let c = null; + const d = () => { + (i.postMessage(!1), clearTimeout(c), (i.onmessage = null)); + }; + return "STREAM" !== r.body + ? (d(), new Response(r.body, r)) + : new Response( + new ReadableStream({ + pull: (s) => + new Promise((t) => { + ((i.onmessage = ({ data: e }) => { + (e ? s.enqueue(e) : (d(), s.close()), t()); + }), + e || + (clearTimeout(c), + "document" !== a && + (c = setTimeout(() => { + (d(), t()); + }, 5e3))), + i.postMessage(!0)); + }), + cancel() { + d(); + }, + }), + r, + ); + })(s) + : null; + })(s); + t && s.respondWith(t); + }), + self.addEventListener("activate", () => { + self.clients.claim(); + })); +})(); diff --git a/server/cleanup.ts b/server/cleanup.ts new file mode 100644 index 0000000..05debe0 --- /dev/null +++ b/server/cleanup.ts @@ -0,0 +1,16 @@ +import { postgres } from "./utils/postgres.ts"; + +cleanupPostgres(); +setInterval(cleanupPostgres, 5 * 60 * 1000); + +async function cleanupPostgres() { + if (!postgres) { + return; + } + console.time("[CLEANUP]"); + const result = await postgres?.query( + `DELETE FROM room WHERE owner IS NULL AND ("lastUpdateTime" < NOW() - INTERVAL '1 day' OR "lastUpdateTime" IS NULL)`, + ); + console.log(result.command, result.rowCount); + console.timeEnd("[CLEANUP]"); +} diff --git a/server/config.ts b/server/config.ts new file mode 100644 index 0000000..8878bb1 --- /dev/null +++ b/server/config.ts @@ -0,0 +1,69 @@ +import { loadEnvFile } from "node:process"; + +try { + loadEnvFile(); +} catch (e) { + console.log(e); +} + +const defaults = { + REDIS_URL: "", // Optional, for metrics + DATABASE_URL: "", // Optional, for permanent rooms and VBrowser management + YOUTUBE_API_KEY: "", // Optional, provide one to enable searching YouTube + NODE_ENV: "", // Usually, you should let process.env.NODE_ENV override this + FIREBASE_ADMIN_SDK_CONFIG: "", // Optional, for features requiring sign-in/authentication + FIREBASE_DATABASE_URL: "", // Optional (unused) + STRIPE_SECRET_KEY: "", // Optional, for subscriptions + VBROWSER_SESSION_SECONDS: 10800, // Number of seconds to allow vbrowsers to run for + VBROWSER_SESSION_SECONDS_LARGE: 86400, // Number of seconds to allow large vbrowsers to run for + VM_POOL_RAMP_DOWN_HOURS: "", // Comma separated start/end UTC hours of the ramp down period + VM_POOL_RAMP_UP_HOURS: "", // Comma separated start/end UTC hours of the ramp up period + VBROWSER_TAG: "", // Optional, tag to put on VBrowser VM instances + DO_TOKEN: "", // Optional, for DigitalOcean VMs + DO_GATEWAY: "", // Gateway handling SSL termination + DO_IMAGE: "", // ID of DigitalOcean snapshot image to use for vbrowser + DO_SSH_KEYS: "", // IDs of DigitalOcean SSH keys to access vbrowsers + HETZNER_TOKEN: "", // Optional, for Hetzner VMs + HETZNER_GATEWAY: "", // Gateway handling SSL termination + HETZNER_SSH_KEYS: "", // IDs of Hetzner SSH keys to access vbrowsers + HETZNER_IMAGE: "", // ID of Hetzner snapshot image to use for vbrowser + VM_MANAGER_CONFIG: "", // Comma-separated list of the pools of VMs to run (provider:size:region:minSize:limitSize:hostname), e.g. Docker:large:US:0:1:localhost,Docker:standard:US:0:1:localhost + SCW_SECRET_KEY: "", // Optional, for Scaleway VMs + SCW_ORGANIZATION_ID: "", // Optional, for Scaleway VMs + SCW_GATEWAY: "", // Gateway handling SSL termination + SCW_IMAGE: "", // ID of Scaleway snapshot image to use for vbrowser + DOCKER_VM_HOST: "localhost", // Optional, for Docker VMs + DOCKER_VM_HOST_SSH_USER: "root", // Optional, username for Docker host + DOCKER_VM_HOST_SSH_KEY_BASE64: "", // Optional, private SSH key for Docker host, or default to ~/.ssh/id_rsa content + SSL_KEY_FILE: "", // Optional, Filename of SSL key (to use https) + SSL_CRT_FILE: "", // Optional, Filename of SSL cert (to use https) + PORT: 8080, // Port to use for server + HOST: "0.0.0.0", // Host interface to bind server to + STATS_KEY: "", // Secret string to validate viewing stats + BETA_USER_EMAILS: "", // Comma-delimited list of user emails to include in the beta + CUSTOM_SETTINGS_HOSTNAME: "", // Hostname to send different config settings to client + STREAM_PATH: "", // Path of server that supports additional video streams + CONVERT_PATH: "", // Path of server that supports video conversion + ROOM_CAPACITY: 0, // Maximum capacity of a standard room. Set to 0 for unlimited. + ROOM_CAPACITY_SUB: 0, // Maximum capacity of a sub room. Set to 0 for unlimited. + BUILD_DIRECTORY: "build", // Name of the directory where the built React UI is served from + VM_MIN_UPTIME_MINUTES: 0, // Number of minutes of the hour VMs must exist for before being eligible for termination + SHARD: undefined, // Shard ID of the web server (configure in ecosystem.config.js) + FREE_ROOM_LIMIT: 1, // The maximum number of rooms a free user can have + SUBSCRIBER_ROOM_LIMIT: 20, // The maximum number of rooms a subscriber can have + VMWORKER_PORT: 3100, // Port to use for the vmWorker HTTP server + VM_ASSIGNMENT_TIMEOUT: 75, // Number of seconds to wait for a VM before failing + DISCORD_BOT_TOKEN: "", // Token for the Discord bot that generates WatchParty links + DISCORD_ADMIN_BOT_TOKEN: "", // Optional, for Discord bot to set subscriber roles + DISCORD_ADMIN_BOT_SERVER_ID: "708181150220156929", // Optional, ID of the Discord server + DISCORD_ADMIN_BOT_SUB_ROLE_ID: "722202622345609246", // Optional, ID of subscriber role + MEDIASOUP_SERVER: "", // Optional, URL of the MediaSoup server to broadcast to for larger screen/file shares + TWITCH_PROXY_PATH: "", // Optional, URL of the server that can proxy twitch HLS stream playlists and segments + VBROWSER_ADMIN_KEY: "", // Optional, the key to hit admin endpoints on the vbrowser + OPENSUBTITLES_KEY: "", // Optional, key to OpenSubtitles API +}; + +export default { + ...defaults, + ...process.env, +}; diff --git a/server/discordBot.ts b/server/discordBot.ts new file mode 100644 index 0000000..32779c9 --- /dev/null +++ b/server/discordBot.ts @@ -0,0 +1,41 @@ +import { Client, IntentsBitField, Events } from "discord.js"; +import config from "./config.ts"; +import axios from "axios"; +import { redisCount } from "./utils/redis.ts"; + +// URL to invite bot: https://discord.com/api/oauth2/authorize?client_id=1071394728513380372&permissions=2147485696&scope=bot + +const HOST_NAME = "https://www.watchparty.me"; +const API_NAME = "https://backend.watchparty.me"; + +const client = new Client({ + intents: [IntentsBitField.Flags.Guilds, IntentsBitField.Flags.GuildMessages], +}); + +client.on("ready", () => { + console.log("I am ready!"); + console.log("bot is in %s guilds", client.guilds.cache.size); +}); + +client.on(Events.InteractionCreate, async (interaction) => { + if (!interaction.isChatInputCommand()) return; + + if (interaction.commandName === "watch") { + const preload = interaction.options.get("video")?.value; + // Call the watchparty API to make a room + const response = await axios.post(API_NAME + "/createRoom", { + video: preload, + }); + redisCount("discordBotWatch"); + // Return the generated room URL + await interaction.reply({ + content: `Created a new WatchParty${ + preload ? ` with video ${preload}` : "" + }! +${HOST_NAME + "/watch" + response.data.name} +`, + }); + } +}); + +client.login(config.DISCORD_BOT_TOKEN); diff --git a/server/ecosystem.config.js b/server/ecosystem.config.js new file mode 100644 index 0000000..aa0b171 --- /dev/null +++ b/server/ecosystem.config.js @@ -0,0 +1,81 @@ +export const apps = [ + // { + // name: 'server', + // script: './server/server.ts', + // log_date_format: 'YYYY-MM-DD HH:mm:ss Z', + // interpreter: 'node', + // env: { + // PORT: 80, + // }, + // }, + { + name: "shard1", + script: "./server/server.ts", + log_date_format: "YYYY-MM-DD HH:mm:ss Z", + interpreter: "node", + env: { + SHARD: 1, + PORT: 3001, + }, + }, + { + name: "shard2", + script: "./server/server.ts", + log_date_format: "YYYY-MM-DD HH:mm:ss Z", + interpreter: "node", + env: { + SHARD: 2, + PORT: 3002, + }, + }, + // { + // name: 'shard3', + // script: './server/server.ts', + // log_date_format: 'YYYY-MM-DD HH:mm:ss Z', + // interpreter: 'node', + // env: { + // SHARD: 3, + // PORT: 3003, + // }, + // }, + { + name: "vmWorker", + script: "./server/vmWorker.ts", + log_date_format: "YYYY-MM-DD HH:mm:ss Z", + interpreter: "node", + env: { + HETZNER_GATEWAY: "gateway2.watchparty.me", + HETZNER_SSH_KEYS: "1570536", + HETZNER_IMAGE: "398857306", + SCW_GATEWAY: "gateway2.watchparty.me", + SCW_IMAGE: "172bd9df-eba5-44e7-add0-f6edbb0f9c64", + DO_GATEWAY: "gateway2.watchparty.me", + DO_IMAGE: "150334605", + DO_SSH_KEYS: "cc:3d:a7:d3:99:17:fe:b7:dd:59:c4:78:14:d4:02:d1", + }, + }, + { + name: "syncSubs", + script: "./server/syncSubs.ts", + log_date_format: "YYYY-MM-DD HH:mm:ss Z", + interpreter: "node", + }, + { + name: "timeSeries", + script: "./server/timeSeries.ts", + log_date_format: "YYYY-MM-DD HH:mm:ss Z", + interpreter: "node", + }, + { + name: "cleanup", + script: "./server/cleanup.ts", + log_date_format: "YYYY-MM-DD HH:mm:ss Z", + interpreter: "node", + }, + { + name: "discordBot", + script: "./server/discordBot.ts", + log_date_format: "YYYY-MM-DD HH:mm:ss Z", + interpreter: "node", + }, +]; diff --git a/server/index.d.ts b/server/index.d.ts new file mode 100644 index 0000000..782b9a9 --- /dev/null +++ b/server/index.d.ts @@ -0,0 +1,41 @@ +interface YoutubeResult { + kind: string; + etag: string; + snippet: { + publishedAt: string; + channelId: string; + title: string; + description: string; + thumbnails: { + default: { + url: string; + width: number; + height: number; + }; + medium: { + url: string; + width: number; + height: number; + }; + high: { + url: string; + width: number; + height: number; + }; + standard: { + url: string; + width: number; + height: number; + }; + }; + channelTitle: string; + tags: string[]; + categoryId: string; + liveBroadcastContent: string; + localized: { + title: string; + description: string; + }; + defaultAudioLanguage: string; + }; +} diff --git a/server/room.ts b/server/room.ts new file mode 100644 index 0000000..9d0070e --- /dev/null +++ b/server/room.ts @@ -0,0 +1,1457 @@ +import config from "./config.ts"; +import axios from "axios"; +import { Server, Socket } from "socket.io"; +import { getUser, validateUserToken } from "./utils/firebase.ts"; +import { redis, redisCount, redisCountDistinct } from "./utils/redis.ts"; +import { getIsSubscriberByEmail } from "./utils/stripe.ts"; +import { type AssignedVM } from "./vm/base.ts"; +import { getStartOfDay } from "./utils/time.ts"; +import { postgres, updateObject, upsertObject } from "./utils/postgres.ts"; +import { + fetchYoutubeVideo, + getYoutubeVideoID, + isYouTube, +} from "./utils/youtube.ts"; +//@ts-expect-error +import twitch from "twitch-m3u8"; +import { type QueryResult } from "pg"; +import { Docker } from "./vm/docker.ts"; + +// Stateless pool instance to use for VMs if full management isn't needed +let stateless: Docker | undefined = undefined; +if (!config.VM_MANAGER_CONFIG) { + stateless = new Docker({ + provider: "Docker", + isLarge: false, + region: "US", + limitSize: 0, + minSize: 0, + hostname: config.DOCKER_VM_HOST, + }); +} + +// Extend the interface +declare module "socket.io" { + interface Socket { + clientId: string; + uid: string; + isSub: boolean; + } +} + +export class Room { + // Serialized state + public video: string | null = ""; + public videoTS = 0; + public subtitle = ""; + public playbackRate = 1; + public paused = false; + public loop = false; + private chat: ChatMessage[] = []; + private nameMap: StringDict = {}; + private pictureMap: StringDict = {}; + public vBrowser: AssignedVM | undefined = undefined; + public creator: string | undefined = undefined; // email of the user who created the room (just used for stats) + public lock: string | undefined = undefined; // uid of the user who locked the room + public playlist: PlaylistVideo[] = []; + + // Non-serialized state + public roomId: string; + public roster: User[] = []; + private lastTsMap = Date.now(); + private tsMap: NumberDict = {}; + private io: Server; + private socketIdMap: StringDict = {}; + private tsInterval: NodeJS.Timeout | undefined = undefined; + public isChatDisabled: boolean | undefined = undefined; + public lastUpdateTime: Date = new Date(); + private preventTSUpdate = false; + // Not really a queue since there's no ordering, we just retry as long as this is set + // If we want a real queue then we need external processing of the jobs and a way to update the room from outside + public vBrowserQueue: + | { + roomId: string; + queueTime: Date; + isLarge: boolean; + region: string; + uid: string; + clientId: string; + } + | undefined = undefined; + + constructor( + io: Server, + roomId: string, + roomData?: string | null | undefined, + ) { + this.roomId = roomId; + this.io = io; + + if (roomData) { + this.deserialize(roomData); + } + + this.tsInterval = setInterval(async () => { + // console.log(roomId, this.video, this.roster, this.tsMap, this.nameMap); + // Clean up the data of users who aren't in the room anymore + const memberIds = this.roster.map((p) => p.id); + Object.keys(this.tsMap).forEach((key) => { + if (!memberIds.includes(key)) { + delete this.tsMap[key]; + } + }); + if (this.video) { + this.lastTsMap = Date.now(); + io.of(roomId).emit("REC:tsMap", this.tsMap); + } + }, 1000); + + io.of(roomId).use(async (socket, next) => { + if (postgres) { + const result = await postgres.query( + `SELECT password, owner, "isSubRoom" FROM room where "roomId" = $1`, + [this.roomId], + ); + const password = socket.handshake.query?.password; + // Check if user has the password + const roomPassword = result.rows[0]?.password; + if (roomPassword && password !== roomPassword) { + next(new Error("password")); + return; + } + // Check if room is at capacity + const isSubRoom = result.rows[0]?.isSubRoom; + const roomCapacity = isSubRoom + ? config.ROOM_CAPACITY_SUB + : config.ROOM_CAPACITY; + if (roomCapacity && this.roster.length >= roomCapacity) { + next(new Error("This room is full")); + return; + } + } + // clientId is meant for things that shouldn't require login + // Anything sensitive (e.g. subscriber features, room lock) should be validated with uid and require login + // vbrowser controller, identify chat messages, video chat/screenshare signaling + // Used as keys for ephemeral room state (e.g. name, picture, timestamp) + + // redis-based clientId spoof protection (session) + // Keep a map of clientIds to sessionIDs (a secret generated by client and stored in localstorage) + // If a clientId already exists in map, a matching sessionId must be provided, otherwise fail + // Otherwise, store it with some expiry + // Refresh the expiry on each successful connection + // Attacker can't spoof unless the user doesn't connect for a long time + + // ALTERNATIVE: using crypto? + // What if we send back the client an encrypt or hmac of their clientID? + // Client can store in localstorage + // Can't be spoofed without the server's encryption key + // Attacker could try to bruteforce the key by trying all possibilities + // Client sends both the clear clientId and the hmac + // During transition, accept requests with no hmac + // We would need to turn on enforcement after a while (after all clients have updated) + // On connection, compute hmac of clear clientId and verify it matches what client sent + // We can accept query param clientHmac? + + const clientId = socket.handshake.query?.clientId; + const sessionId = socket.handshake.auth.sessionId; + if (typeof clientId !== "string") { + next(new Error("Invalid clientId type")); + return; + } + // validate clientId is UUID, prevents prototype pollution + if (!isValidUUID(clientId)) { + next(new Error("Invalid clientId format")); + return; + } + // If Redis isn't enabled we'll just allow + if (redis) { + const key = "session:" + clientId; + const savedSession = await redis.get(key); + if (savedSession) { + // passed ID must match, otherwise error + if (savedSession !== sessionId) { + next(new Error("Incorrect sessionId")); + return; + } else { + // Refresh expiry + await redis.expire(key, 60 * 24 * 7); + } + } else { + // Create new session + if (sessionId) { + await redis.setex(key, 60 * 24 * 7, sessionId); + } + } + } + + // Disconnect other sockets with this clientId + if (this.socketIdMap[clientId]) { + io.of(roomId).sockets.get(this.socketIdMap[clientId])?.disconnect(); + } + // Keep track of the current socketID associated with this client (only used for signaling and kicking) + this.socketIdMap[clientId] = socket.id; + if (!this.roster.find(user => user.id === clientId)) { + this.roster.push({ id: clientId }); + } + + next(); + }); + io.of(roomId).on("connection", async (socket: Socket) => { + const clientId = socket.handshake.query?.clientId; + if (typeof clientId !== "string") { + // We already validated in middleware above, this is just to satisfy TS + return; + } + + redisCount("connectStarts"); + redisCountDistinct("connectStartsDistinct", clientId); + + socket.emit("REC:host", this.getHostState()); + socket.emit("REC:nameMap", this.nameMap); + socket.emit("REC:pictureMap", this.pictureMap); + socket.emit("REC:tsMap", this.tsMap); + socket.emit("REC:lock", this.lock); + socket.emit("chatinit", this.chat); + socket.emit("playlist", this.playlist); + this.getRoomState(socket); + io.of(roomId).emit("roster", this.getRosterForApp()); + + socket.clientId = clientId; + socket.uid = ""; + socket.isSub = false; + + // Check if this socket matches this.lock UID + const validateLock = () => { + return !this.lock || socket.uid === this.lock; + }; + + // Check if this socket matches the room owner UID + const validateOwner = async () => { + const result = await postgres?.query( + 'SELECT owner FROM room where "roomId" = $1', + [this.roomId], + ); + const owner = result?.rows[0]?.owner; + return !owner || socket.uid === owner; + }; + + socket.on("CMD:name", (data: unknown) => + this.changeUserName(socket, String(data)), + ); + socket.on("CMD:picture", (data: unknown) => + this.changeUserPicture(socket, String(data)), + ); + socket.on("CMD:uid", async (raw: unknown) => { + let data = raw as { uid: string; token: string }; + // Called when the user logs in, sets the socket's auth state + if (!data || !data.uid || !data.token) { + return; + } + const decoded = await validateUserToken(data.uid, data.token); + if (decoded?.uid) { + // This socket is now confirmed to be this UID + socket.uid = decoded?.uid; + } + const isSubscriber = await getIsSubscriberByEmail(decoded?.email); + if (isSubscriber) { + socket.isSub = true; + } + }); + socket.on("CMD:host", (data: unknown) => { + validateLock() && this.startHosting(socket, String(data)); + }); + socket.on("CMD:play", () => { + validateLock() && this.playVideo(socket); + }); + socket.on("CMD:pause", () => { + validateLock() && this.pauseVideo(socket); + }); + socket.on("CMD:seek", (data: unknown) => { + validateLock() && this.seekVideo(socket, Number(data)); + }); + socket.on("CMD:playbackRate", (data: unknown) => { + validateLock() && this.setPlaybackRate(socket, Number(data)); + }); + socket.on("CMD:loop", (data: unknown) => { + validateLock() && this.setLoop(Boolean(data)); + }); + socket.on("CMD:ts", (data: unknown) => + this.setTimestamp(socket, Number(data)), + ); + socket.on("CMD:chat", (data: unknown) => + this.sendChatMessage(socket, String(data)), + ); + socket.on("CMD:chatV2", (data: unknown) => + this.sendChatMessage(socket, data), + ); + socket.on("CMD:addReaction", (data: unknown) => + this.addReaction(socket, data), + ); + socket.on("CMD:removeReaction", (data: unknown) => { + this.removeReaction(socket, data); + }); + socket.on("CMD:joinVideo", () => this.joinVideo(socket)); + socket.on("CMD:leaveVideo", () => this.leaveVideo(socket)); + socket.on("CMD:joinScreenShare", (data) => { + validateLock() && this.joinScreenSharing(socket, data); + }); + socket.on("CMD:userMute", (data: unknown) => + this.setUserMute(socket, data), + ); + socket.on("CMD:leaveScreenShare", () => this.leaveScreenSharing(socket)); + socket.on("CMD:startVBrowser", (data: unknown) => { + validateLock() && this.startVBrowser(socket, data); + }); + socket.on("CMD:stopVBrowser", () => { + validateLock() && this.stopVBrowser(); + }); + socket.on("CMD:changeController", (data: unknown) => { + validateLock() && this.changeController(String(data)); + }); + socket.on("CMD:subtitle", (data: unknown) => { + validateLock() && this.addSubtitles(String(data)); + }); + socket.on("CMD:lock", async (data: unknown) => { + const hasLock = validateLock(); + const isOwner = await validateOwner(); + (hasLock || isOwner) && this.lockRoom(socket, data); + }); + socket.on("CMD:askHost", () => { + socket.emit("REC:host", this.getHostState()); + }); + socket.on("CMD:getRoomState", () => this.getRoomState(socket)); + socket.on("CMD:setRoomState", async (data: unknown) => { + (await validateOwner()) && this.setRoomState(socket, data); + }); + socket.on("CMD:setRoomOwner", async (data: unknown) => { + (await validateOwner()) && this.setRoomOwner(socket, data); + }); + socket.on("CMD:playlistNext", (data: unknown) => { + validateLock() && this.playlistNext(data); + }); + socket.on("CMD:playlistAdd", (data: unknown) => { + validateLock() && this.playlistAdd(socket, String(data)); + }); + socket.on("CMD:playlistMove", (data: unknown) => { + validateLock() && this.playlistMove(data); + }); + socket.on("CMD:playlistDelete", (data: unknown) => { + validateLock() && this.playlistDelete(Number(data)); + }); + socket.on("CMD:kickUser", async (data: unknown) => { + (await validateOwner()) && this.kickUser(data); + }); + socket.on("CMD:deleteChatMessages", async (data: unknown) => { + (await validateOwner()) && this.deleteChatMessages(data); + }); + + socket.on("signal", (data: unknown) => + this.sendSignal(socket, data, "signal"), + ); + socket.on("signalSS", (data: unknown) => + this.sendSignal(socket, data, "signalSS"), + ); + + socket.on("disconnect", () => this.onDisconnect(socket)); + }); + } + + public serialize = () => { + // Get the set of IDs with messages in chat + // Only serialize roster and picture ID for those people, to save space + const chatIDs = new Set(this.chat.map((msg) => msg.id)); + const abbrNameMap: StringDict = {}; + Object.keys(this.nameMap).forEach((id) => { + if (chatIDs.has(id)) { + abbrNameMap[id] = this.nameMap[id]; + } + }); + const abbrPictureMap: StringDict = {}; + Object.keys(this.pictureMap).forEach((id) => { + if (chatIDs.has(id)) { + abbrPictureMap[id] = this.pictureMap[id]; + } + }); + return JSON.stringify({ + video: this.video, + videoTS: this.videoTS, + subtitle: this.subtitle, + playbackRate: this.playbackRate, + paused: this.paused, + chat: this.chat, + nameMap: abbrNameMap, + pictureMap: abbrPictureMap, + vBrowser: this.vBrowser, + lock: this.lock, + creator: this.creator, + playlist: this.playlist, + loop: this.loop, + }); + }; + + private deserialize = (roomData: string) => { + const roomObj = JSON.parse(roomData); + this.video = roomObj.video; + this.videoTS = roomObj.videoTS; + if (roomObj.subtitle) { + this.subtitle = roomObj.subtitle; + } + if (roomObj.paused) { + this.paused = roomObj.paused; + } + if (roomObj.chat) { + this.chat = roomObj.chat; + } + if (roomObj.nameMap) { + this.nameMap = roomObj.nameMap; + } + if (roomObj.pictureMap) { + this.pictureMap = roomObj.pictureMap; + } + if (roomObj.vBrowser) { + this.vBrowser = roomObj.vBrowser; + } + if (roomObj.lock) { + this.lock = roomObj.lock; + } + if (roomObj.creator) { + this.creator = roomObj.creator; + } + if (roomObj.playlist) { + this.playlist = roomObj.playlist; + } + if (roomObj.playbackRate) { + this.playbackRate = roomObj.playbackRate; + } + if (roomObj.loop) { + this.loop = roomObj.loop; + } + }; + + public saveRoom = async () => { + if (postgres) { + try { + const roomString = this.serialize(); + await postgres.query( + `UPDATE room SET + "lastUpdateTime" = $1, data = $2 + WHERE "roomId" = $3`, + [this.lastUpdateTime ?? new Date(), roomString, this.roomId], + ); + } catch (e) { + console.warn(e); + } + } + }; + + public destroy = () => { + if (this.tsInterval) { + clearInterval(this.tsInterval); + } + }; + + public getRosterForStats = () => { + return this.roster.map((p) => ({ + id: p.id, + name: this.nameMap[p.id] || p.id, + ts: this.tsMap[p.id], + // TODO this will not work behind nginx reverse proxy, pass it and read from X-Real-IP instead + // socket.handshake.headers["x-real-ip"] + // ip: this.io.of(this.roomId).sockets.get(p.id)?.request?.socket + // ?.remoteAddress, + })); + }; + + protected getSharerId = (): string => { + let sharerId = ""; + if (this.video?.startsWith("screenshare://")) { + sharerId = this.video?.slice("screenshare://".length).split("@")[0]; + } else if (this.video?.startsWith("fileshare://")) { + sharerId = this.video?.slice("fileshare://".length).split("@")[0]; + } + return sharerId; + }; + + protected getRosterForApp = (): User[] => { + return this.roster.map((p) => { + return { + ...p, + isScreenShare: p.id === this.getSharerId(), + }; + }); + }; + + private getHostState = (): HostState => { + return { + video: this.video ?? "", + videoTS: this.videoTS, + subtitle: this.subtitle, + playbackRate: this.playbackRate, + paused: this.paused, + isVBrowserLarge: Boolean(this.vBrowser && this.vBrowser.large), + controller: this.vBrowser?.controllerClient, + loop: this.loop, + }; + }; + + public stopVBrowserInternal = async () => { + const assignTime = this.vBrowser && this.vBrowser.assignTime; + const id = this.vBrowser?.id; + const provider = this.vBrowser?.provider; + const isLarge = this.vBrowser?.large ?? false; + const region = this.vBrowser?.region ?? ""; + const uid = this.vBrowser?.creatorUID ?? ""; + this.vBrowser = undefined; + this.cmdHost(null, ""); + // Force a save because this might change in unattended rooms + this.lastUpdateTime = new Date(); + this.saveRoom(); + if (redis && assignTime) { + await redis.lpush("vBrowserSessionMS", Date.now() - assignTime); + await redis.ltrim("vBrowserSessionMS", 0, 19); + } + + if (id) { + try { + if (stateless) { + await stateless.terminateVM(id); + } else { + await axios.post( + "http://localhost:" + config.VMWORKER_PORT + "/releaseVM", + { + provider, + isLarge, + region, + id, + roomId: this.roomId, + }, + ); + } + } catch (e) { + console.warn(e); + } + } + }; + + private cmdHost = (socket: Socket | null, data: string) => { + if (data && data.length > 50000) { + return; + } + this.video = data; + this.videoTS = 0; + this.paused = false; + this.subtitle = ""; + this.loop = false; + this.playbackRate = 1; + this.tsMap = {}; + this.preventTSUpdate = true; + setTimeout(() => (this.preventTSUpdate = false), 1000); + this.io.of(this.roomId).emit("REC:tsMap", this.tsMap); + this.io.of(this.roomId).emit("REC:host", this.getHostState()); + if (socket && data) { + const chatMsg = { id: socket.clientId, cmd: "host", msg: data }; + this.addChatMessage(socket, chatMsg); + } + if (data === "") { + this.playlistNext(null); + } + // The room video is changing so remove room from vbrowser queue + this.vBrowserQueue = undefined; + // Resend the roster (updates screenshare state etc) + this.io.of(this.roomId).emit("roster", this.getRosterForApp()); + }; + + public addChatMessage = (socket: Socket | null, chatMsg: ChatMessageBase) => { + if (this.isChatDisabled && !chatMsg.cmd) { + return; + } + const chatWithTime: ChatMessage = { + ...chatMsg, + timestamp: new Date().toISOString(), + videoTS: socket?.clientId ? this.tsMap[socket.clientId] : undefined, + }; + if (socket?.isSub) { + chatWithTime.isSub = true; + } + this.chat.push(chatWithTime); + this.chat = this.chat.splice(-100); + this.io.of(this.roomId).emit("REC:chat", chatWithTime); + }; + + private changeUserName = (socket: Socket, data: string) => { + if (!data) { + return; + } + if (data && data.length > 50) { + return; + } + this.nameMap[socket.clientId] = data; + this.io.of(this.roomId).emit("REC:nameMap", this.nameMap); + }; + + private changeUserPicture = (socket: Socket, data: string) => { + if (data && data.length > 10000) { + return; + } + this.pictureMap[socket.clientId] = data; + this.io.of(this.roomId).emit("REC:pictureMap", this.pictureMap); + }; + + private startHosting = async (socket: Socket, data: string) => { + if (this.vBrowser) { + socket.emit( + "errorMessage", + `Can't update the video while vbrowser is running`, + ); + return; + } + redisCount("urlStarts"); + if (config.STREAM_PATH && data?.startsWith(config.STREAM_PATH)) { + redisCount("streamStarts"); + } + if (config.CONVERT_PATH && data?.startsWith(config.CONVERT_PATH)) { + redisCount("convertStarts"); + } + // If a reddit URL, extract video URL + if ( + data?.startsWith("https://www.reddit.com") || + data?.startsWith("https://old.reddit.com") || + data?.startsWith("https://reddit.com") + ) { + if (data.endsWith("/")) { + // Remove trailing slash + data = data.slice(0, -1); + } + data = data + ".json"; + // Extract fallback_url + const resp = await axios.get(data); + const json = resp.data; + let reddit_m3u8 = + json?.[0]?.data?.children?.[0]?.data?.secure_media?.reddit_video + ?.hls_url; + let reddit_mp4 = + json?.[0]?.data?.children?.[0]?.data?.secure_media?.reddit_video + ?.fallback_url; + // prefer reddit m3u8 streams over the mp4 links as the m3u8 streams contain audio. + data = reddit_m3u8 || reddit_mp4 || data; + } else if ( + data?.startsWith("https://www.twitch.tv") || + data?.startsWith("https://twitch.tv") + ) { + try { + // Extract m3u8 data + // Note this won't work directly since Twitch will reject requests from the wrong origin--need to proxy the m3u8 playlist + const channel = data.split("/").slice(-1)[0]; + const isStream = isNaN(Number(channel)); + let streams = []; + if (isStream) { + streams = await twitch.getStream(channel); + } else { + streams = await twitch.getVod(channel); + } + // console.log(streams); + const target = + streams.find((str: any) => str.quality.includes("(source)")) || + streams[0]; + const parsed = new URL(target?.url); + const newUrl = new URL(config.TWITCH_PROXY_PATH); + newUrl.pathname = "/proxy" + parsed.pathname; + newUrl.searchParams.set("host", parsed.host); + newUrl.searchParams.set("displayName", data); + newUrl.search = newUrl.searchParams.toString(); + data = newUrl.toString(); + } catch (e) { + console.warn(e); + } + } + this.cmdHost(socket, data); + }; + + private playlistNext = (raw: unknown) => { + const data = raw ? String(raw) : null; + // Clients may pass the URL that should be the current one. + // If we've already advanced the playlist, we can ignore duplicate calls + if ( + data && + this.video && + data !== this.video && + getYoutubeVideoID(data) !== getYoutubeVideoID(this.video) + ) { + // Validation didn't match + return; + } + const next = this.playlist.shift(); + this.io.of(this.roomId).emit("playlist", this.playlist); + if (next) { + this.cmdHost(null, next.url); + } + }; + + public playlistAdd = async (socket: Socket | null, data: string) => { + if (data && data.length > 20000) { + return; + } + redisCount("playlistAdds"); + const youtubeVideoId = getYoutubeVideoID(data); + const item = { + name: data, + channel: "Video URL", + duration: 0, + url: data, + type: data.startsWith("magnet:") ? "magnet" : "file", + }; + let video: PlaylistVideo | null = null; + try { + if (youtubeVideoId) { + video = await fetchYoutubeVideo(youtubeVideoId); + } + } catch (e) { + // Failed to fetch YouTube video info but can still add the URL + console.warn(e); + } + if (video) { + this.playlist.push(video); + } else { + this.playlist.push(item); + } + this.io.of(this.roomId).emit("playlist", this.playlist); + const clientId = socket?.clientId; + if (clientId) { + const chatMsg = { + id: clientId, + cmd: "playlistAdd", + msg: data, + }; + this.addChatMessage(socket, chatMsg); + } + if (!this.video) { + this.playlistNext(null); + } + }; + + private playlistDelete = (index: number) => { + if (index !== -1) { + this.playlist.splice(index, 1); + this.io.of(this.roomId).emit("playlist", this.playlist); + } + }; + + private playlistMove = (raw: unknown) => { + const data = raw as { index: number; toIndex: number }; + if (!data) { + return; + } + if (data.index !== -1) { + const items = this.playlist.splice(data.index, 1); + this.playlist.splice(data.toIndex, 0, items[0]); + this.io.of(this.roomId).emit("playlist", this.playlist); + } + }; + + private playVideo = (socket: Socket) => { + socket.broadcast.emit("REC:play", this.video); + const chatMsg = { + id: socket.clientId, + cmd: "play", + msg: this.tsMap[socket.clientId]?.toString(), + }; + this.paused = false; + this.addChatMessage(socket, chatMsg); + }; + + private pauseVideo = (socket: Socket) => { + socket.broadcast.emit("REC:pause"); + const chatMsg = { + id: socket.clientId, + cmd: "pause", + msg: this.tsMap[socket.clientId]?.toString(), + }; + this.paused = true; + this.addChatMessage(socket, chatMsg); + }; + + private seekVideo = (socket: Socket, data: number) => { + if (String(data).length > 100) { + return; + } + this.videoTS = data; + socket.broadcast.emit("REC:seek", data); + const chatMsg = { id: socket.clientId, cmd: "seek", msg: data?.toString() }; + this.addChatMessage(socket, chatMsg); + }; + + private setPlaybackRate = (socket: Socket, data: number) => { + if (String(data).length > 100) { + return; + } + this.playbackRate = Number(data); + this.io.of(this.roomId).emit("REC:playbackRate", Number(data)); + const chatMsg = { + id: socket.clientId, + cmd: "playbackRate", + msg: data?.toString(), + }; + this.addChatMessage(socket, chatMsg); + }; + + private setLoop = (data: boolean) => { + if (String(data).length > 100) { + return; + } + this.loop = data; + this.io.of(this.roomId).emit("REC:loop", data); + }; + + private setTimestamp = (socket: Socket, data: number) => { + if (String(data).length > 100) { + return; + } + // Prevent lagging TS updates from the old video from messing up our timestamps + if (this.preventTSUpdate) { + return; + } + // This is negative for live streams, so allow overwriting + // Otherwise, only increment this value to prevent a lagging viewer from holding up the room state + if (data < 0 || data > this.videoTS) { + this.videoTS = data; + } + // Normalize the received TS based on how long since the last tsMap emit + // Later sends will have higher values so subtract the difference + // Add 1 as we will emit 1 second from the last one + const timeSinceTsMap = Date.now() - this.lastTsMap; + // console.log(socket.clientId, 'offset', offset, 'ms'); + this.tsMap[socket.clientId] = data - timeSinceTsMap / 1000 + 1; + }; + + private isValidChatMessage = (msg: string | undefined) => { + return Boolean(msg && msg.length <= 10000); + }; + + private sendChatMessage = (socket: Socket, raw: unknown) => { + // Support legacy string and V2 object chat payloads. + const payload = typeof raw === "string" ? { msg: raw } : raw; + if (!payload || typeof payload !== "object") { + return; + } + + // Validate supported fields. + const data = payload as Record; + const msg = typeof data.msg === "string" ? data.msg : undefined; + const replyToId = + typeof data.replyToId === "string" ? data.replyToId : undefined; + const replyToTimestamp = + typeof data.replyToTimestamp === "string" + ? data.replyToTimestamp + : undefined; + + if (!msg || !this.isValidChatMessage(msg)) { + return; + } + + // Require both reply fields or neither. + if (Boolean(replyToId) !== Boolean(replyToTimestamp)) { + return; + } + + const baseMsg: ChatMessageBase = { id: socket.clientId, msg }; + const emitChatMessage = (chatMsg: ChatMessageBase) => { + redisCount("chatMessages"); + this.addChatMessage(socket, chatMsg); + }; + + // No reply metadata -> regular message. + if (!replyToId || !replyToTimestamp) { + emitChatMessage(baseMsg); + return; + } + + const target = this.chat.find( + (m) => m.id === replyToId && m.timestamp === replyToTimestamp, + ); + // Missing target -> fall back to regular message. + if (!target) { + emitChatMessage(baseMsg); + return; + } + + emitChatMessage({ + ...baseMsg, + replyToId, + replyToTimestamp, + replyToUserId: replyToId, + replyToMsg: target.msg || "", + }); + }; + + private addReaction = (socket: Socket, raw: unknown) => { + const data = raw as { value: string; msgId: string; msgTimestamp: string }; + if (!data || !data.value || !data.msgId || !data.msgTimestamp) { + return; + } + // Emojis can be multiple bytes + if (data.value.length > 8) { + return; + } + const msg = this.chat.find( + (m) => m.id === data.msgId && m.timestamp === data.msgTimestamp, + ); + if (!msg) { + return; + } + msg.reactions = msg.reactions || {}; + msg.reactions[data.value] = msg.reactions[data.value] || []; + + if (!msg.reactions[data.value].includes(socket.clientId)) { + msg.reactions[data.value].push(socket.clientId); + const reaction: Reaction = { user: socket.clientId, ...data }; + redisCount("addReaction"); + this.io.of(this.roomId).emit("REC:addReaction", reaction); + } + }; + + private removeReaction = (socket: Socket, raw: unknown) => { + const data = raw as { value: string; msgId: string; msgTimestamp: string }; + if (!data || !data.value || !data.msgId || !data.msgTimestamp) { + return; + } + // Emojis can be multiple bytes + if (data.value.length > 8) { + return; + } + const msg = this.chat.find( + (m) => m.id === data.msgId && m.timestamp === data.msgTimestamp, + ); + if (!msg || !msg.reactions?.[data.value]) { + return; + } + msg.reactions[data.value] = msg.reactions[data.value].filter( + (id) => id !== socket.clientId, + ); + const reaction: Reaction = { user: socket.clientId, ...data }; + this.io.of(this.roomId).emit("REC:removeReaction", reaction); + }; + + private joinVideo = async (socket: Socket) => { + const match = this.roster.find((user) => user.id === socket.clientId); + if (match) { + match.isVideoChat = true; + redisCount("videoChatStarts"); + } + this.io.of(this.roomId).emit("roster", this.getRosterForApp()); + }; + + private leaveVideo = async (socket: Socket) => { + const match = this.roster.find((user) => user.id === socket.clientId); + if (match) { + match.isVideoChat = false; + } + this.io.of(this.roomId).emit("roster", this.getRosterForApp()); + }; + + private setUserMute = (socket: Socket, raw: unknown) => { + const data = raw as { isMuted: boolean }; + if (!data) { + return; + } + const match = this.roster.find((user) => user.id === socket.clientId); + if (match) { + match.isMuted = data.isMuted; + } + this.io.of(this.roomId).emit("roster", this.getRosterForApp()); + }; + + private joinScreenSharing = (socket: Socket, raw: unknown) => { + const data = raw as { file: boolean; mediasoup?: boolean }; + if (!data) { + return; + } + const sharer = this.getRosterForApp().find((user) => user.isScreenShare); + if (sharer) { + // Someone's already sharing + socket.emit( + "errorMessage", + "There is already an active share in this room", + ); + return; + } + let mediasoupSuffix = ""; + if (data?.mediasoup) { + // TODO validate the user has permissions to ask for a mediasoup + // TODO set up the room on the remote server rather than letting the remote server create + mediasoupSuffix = + "@" + config.MEDIASOUP_SERVER + "/" + crypto.randomUUID(); + redisCount("mediasoupStarts"); + } + if (data && data.file) { + this.cmdHost(socket, "fileshare://" + socket.clientId + mediasoupSuffix); + redisCount("fileShareStarts"); + } else { + this.cmdHost( + socket, + "screenshare://" + socket.clientId + mediasoupSuffix, + ); + redisCount("screenShareStarts"); + } + this.io.of(this.roomId).emit("roster", this.getRosterForApp()); + }; + + private leaveScreenSharing = (socket: Socket) => { + const sharer = this.getRosterForApp().find((user) => user.isScreenShare); + if (!sharer || sharer?.id !== socket.clientId) { + socket.emit("errorMessage", "Not the active sharer"); + return; + } + this.cmdHost(socket, ""); + this.io.of(this.roomId).emit("roster", this.getRosterForApp()); + }; + + private startVBrowser = async (socket: Socket, raw: unknown) => { + const data = raw as { + options?: { size: string; region: string; provider: string }; + }; + if (!data) { + socket.emit("errorMessage", "Invalid vBrowser input"); + return; + } + const { clientId, uid, isSub } = socket; + // these checks are skipped if firebase not provided + if (config.FIREBASE_ADMIN_SDK_CONFIG) { + const user = await getUser(uid); + // Validate verified email if not a third-party auth provider + if ( + user?.providerData[0].providerId === "password" && + !user?.emailVerified + ) { + socket.emit( + "errorMessage", + "A verified email is required to start a VBrowser.", + ); + return; + } + + // Log the vbrowser creation by uid and clientid + if (redis) { + const expireTime = getStartOfDay() / 1000 + 86400; + if (clientId) { + const clientCount = await redis.zincrby( + "vBrowserClientIDs", + 1, + clientId, + ); + redis.expireat("vBrowserClientIDs", expireTime); + const clientMinutes = await redis.zincrby( + "vBrowserClientIDMinutes", + 1, + clientId, + ); + redis.expireat("vBrowserClientIDMinutes", expireTime); + } + if (uid) { + const uidCount = await redis.zincrby("vBrowserUIDs", 1, uid); + redis.expireat("vBrowserUIDs", expireTime); + const uidMinutes = await redis.zincrby("vBrowserUIDMinutes", 1, uid); + redis.expireat("vBrowserUIDMinutes", expireTime); + // TODO limit users based on client or uid usage + } + } + // check if the user already has a VM already in postgres + if (postgres) { + const { rows } = await postgres.query( + "SELECT count(1) from vbrowser WHERE uid = $1", + [uid], + ); + if (rows[0].count >= 2) { + socket.emit( + "errorMessage", + "There is already an active vBrowser for this user.", + ); + return; + } + } + } + let isLarge = false; + let region = ""; + // Check if user is subscriber or firebase not configured, if so allow sub options + if (isSub || !config.FIREBASE_ADMIN_SDK_CONFIG) { + isLarge = data.options?.size === "large"; + if (data.options?.region) { + region = data.options?.region; + } + } + + redisCount("vBrowserStarts"); + this.cmdHost(socket, "vbrowser://"); + // Put the room in the vbrowser queue + this.vBrowserQueue = { + roomId: this.roomId, + queueTime: new Date(), + isLarge, + region, + uid, + clientId, + }; + // Check if a vbrowser is available + while (this.vBrowserQueue) { + const { queueTime, isLarge, region, uid, roomId, clientId } = + this.vBrowserQueue; + let assignment: AssignedVM | undefined = undefined; + try { + if (stateless) { + const pass = crypto.randomUUID(); + const id = await stateless.startVM(pass); + assignment = { + ...(await stateless.getVM(id)), + pass, + assignTime: Date.now(), + }; + } else { + const { data } = await axios.post( + "http://localhost:" + config.VMWORKER_PORT + "/assignVM", + { + isLarge, + region, + uid, + roomId, + }, + ); + assignment = data; + } + } catch (e) { + console.warn(e); + } + if (assignment) { + this.vBrowser = assignment; + this.vBrowser.controllerClient = clientId; + this.vBrowser.creatorUID = uid; + this.vBrowser.creatorClientID = clientId; + const assignEnd = Date.now(); + const assignElapsed = assignEnd - Number(queueTime); + await redis?.lpush("vBrowserStartMS", assignElapsed); + await redis?.ltrim("vBrowserStartMS", 0, 19); + console.log( + "[ASSIGN] %s to %s in %s", + assignment.provider + ":" + assignment.id, + roomId, + assignElapsed + "ms", + ); + this.cmdHost( + null, + "vbrowser://" + this.vBrowser.pass + "@" + this.vBrowser.host, + ); + } + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + }; + + private stopVBrowser = async () => { + if (!this.vBrowser && this.video !== "vbrowser://") { + return; + } + await this.stopVBrowserInternal(); + redisCount("vBrowserTerminateManual"); + }; + + private changeController = (data: string) => { + if (data && data.length > 100) { + return; + } + if (this.vBrowser) { + this.vBrowser.controllerClient = data; + this.io.of(this.roomId).emit("REC:changeController", data); + } + }; + + private addSubtitles = async (data: string) => { + if (data && data.length > 10000) { + return; + } + this.subtitle = data; + this.io.of(this.roomId).emit("REC:subtitle", this.subtitle); + }; + + private lockRoom = async (socket: Socket, raw: unknown) => { + const data = raw as { locked: boolean }; + if (!data) { + return; + } + const { uid, clientId } = socket; + this.lock = data.locked ? uid : ""; + this.io.of(this.roomId).emit("REC:lock", this.lock); + const chatMsg = { + id: clientId, + cmd: data.locked ? "lock" : "unlock", + msg: "", + }; + this.addChatMessage(socket, chatMsg); + }; + + private setRoomOwner = async (socket: Socket, raw: unknown) => { + const data = raw as { + undo: boolean; + }; + if (!data) { + return; + } + if (!postgres) { + socket.emit("errorMessage", "Database is not available"); + return; + } + const { uid, isSub } = socket; + if (data.undo) { + await updateObject( + postgres, + "room", + { + password: null, + owner: null, + vanity: null, + isChatDisabled: null, + isSubRoom: null, + roomTitle: null, + roomDescription: null, + roomTitleColor: null, + mediaPath: null, + }, + { roomId: this.roomId }, + ); + socket.emit("REC:getRoomState", {}); + } else { + // validate room count + const roomCount = ( + await postgres.query( + 'SELECT count(1) from room where owner = $1 AND "roomId" != $2', + [uid, this.roomId], + ) + ).rows[0].count; + const limit = isSub + ? config.SUBSCRIBER_ROOM_LIMIT + : config.FREE_ROOM_LIMIT; + if (roomCount >= limit) { + socket.emit( + "errorMessage", + `You've exceeded the permanent room limit. Subscribe for additional permanent rooms.`, + ); + return; + } + const roomObj = { + roomId: this.roomId, + owner: uid, + isSubRoom: isSub, + }; + let result: QueryResult | null = null; + result = await upsertObject(postgres, "room", roomObj, { + roomId: true, + }); + const row = result?.rows?.[0]; + // console.log(result, row); + socket.emit("REC:getRoomState", { + password: row?.password, + vanity: row?.vanity, + owner: row?.owner, + }); + } + }; + + private getRoomState = async (socket: Socket) => { + if (!postgres) { + return; + } + const result = await postgres.query( + `SELECT password, vanity, owner, "isChatDisabled", "roomTitle", "roomDescription", "roomTitleColor", "mediaPath" FROM room where "roomId" = $1`, + [this.roomId], + ); + const first = result.rows[0]; + if (this.isChatDisabled === undefined) { + this.isChatDisabled = Boolean(first?.isChatDisabled); + } + // TODO only send the password if this is current owner + socket.emit("REC:getRoomState", { + password: first?.password, + vanity: first?.vanity, + owner: first?.owner, + isChatDisabled: first?.isChatDisabled, + roomTitle: first?.roomTitle, + roomDescription: first?.roomDescription, + roomTitleColor: first?.roomTitleColor, + mediaPath: first?.mediaPath, + }); + }; + + private setRoomState = async (socket: Socket, raw: unknown) => { + const data = raw as { + password: string; + vanity: string; + isChatDisabled: boolean; + roomTitle: string; + roomDescription: string; + roomTitleColor: string; + mediaPath: string; + }; + if (!postgres) { + socket.emit("errorMessage", "Database is not available"); + return; + } + if (!data) { + return; + } + const { + password, + vanity, + isChatDisabled, + roomTitle, + roomDescription, + roomTitleColor, + mediaPath, + } = data; + if (password) { + if (password.length > 100) { + socket.emit("errorMessage", "Password too long"); + return; + } + } + if (vanity && vanity.length > 100) { + socket.emit("errorMessage", "Custom URL too long"); + return; + } + if (roomTitle && roomTitle.length > 50) { + socket.emit("errorMessage", "Room title too long"); + return; + } + if (roomDescription && roomDescription.length > 120) { + socket.emit("errorMessage", "Room description too long"); + return; + } + // check if is valid hex color representation + if (!/^#([0-9a-f]{3}){1,2}$/i.test(roomTitleColor)) { + socket.emit("errorMessage", "Invalid color code"); + return; + } + if (mediaPath && mediaPath.length > 1000) { + socket.emit("errorMessage", "Media source too long"); + return; + } + // console.log(owner, vanity, password); + const roomObj: any = { + roomId: this.roomId, + password: password, + isChatDisabled: isChatDisabled, + mediaPath: mediaPath, + }; + const { isSub, uid } = socket; + if (isSub) { + // user must be sub to set certain properties + // If empty vanity, reset to null + roomObj.vanity = vanity ?? null; + roomObj.roomTitle = roomTitle; + roomObj.roomDescription = roomDescription; + roomObj.roomTitleColor = roomTitleColor; + } + try { + const query = `UPDATE room + SET ${Object.keys(roomObj).map((k, i) => `"${k}" = $${i + 1}`)} + WHERE "roomId" = $${Object.keys(roomObj).length + 1} + AND owner = $${Object.keys(roomObj).length + 2} + RETURNING *`; + const result = await postgres.query(query, [ + ...Object.values(roomObj), + this.roomId, + uid, + ]); + const row = result.rows[0]; + this.isChatDisabled = Boolean(row?.isChatDisabled); + // TODO only send password if current owner + this.io.of(this.roomId).emit("REC:getRoomState", { + password: row?.password, + vanity: row?.vanity, + owner: row?.owner, + isChatDisabled: row?.isChatDisabled, + roomTitle: row?.roomTitle, + roomDescription: row?.roomDescription, + roomTitleColor: row?.roomTitleColor, + mediaPath: row?.mediaPath, + }); + socket.emit("successMessage", "Saved admin settings"); + } catch (e) { + console.warn(e); + } + }; + + private sendSignal = ( + socket: Socket, + raw: unknown, + eventName: "signal" | "signalSS", + ) => { + const data = raw as { to: string; msg: string; sharer?: boolean }; + if (!data) { + return; + } + const fromClientId = socket.clientId; + const toId = this.socketIdMap[data.to]; + if (toId) { + this.io.of(this.roomId).to(toId).emit(eventName, { + from: fromClientId, + msg: data.msg, + sharer: data.sharer, + }); + } + }; + + private onDisconnect = (socket: Socket) => { + const { clientId } = socket; + // Disconnecting socket is the current one + if (socket.id === this.socketIdMap[clientId]) { + let index = this.roster.findIndex((user) => user.id === clientId); + if (index > -1) { + this.roster.splice(index, 1); + } + this.io.of(this.roomId).emit("roster", this.getRosterForApp()); + delete this.tsMap[clientId]; + delete this.socketIdMap[clientId]; + } + // Keep namemap/picturemap so old chat messages still render correctly after disconnect + // When serializing we only write values with messages in chat + // This will keep growing in memory until the room is unloaded + }; + + private kickUser = async (raw: unknown) => { + const data = raw as { userToBeKicked: string }; + if (!data) { + return; + } + const userToBeKickedSocket = this.io + .of(this.roomId) + .sockets.get(this.socketIdMap[data.userToBeKicked]); + if (userToBeKickedSocket) { + userToBeKickedSocket.emit("kicked"); + userToBeKickedSocket.disconnect(); + } + }; + + private deleteChatMessages = async (raw: unknown) => { + const data = raw as { + author: string; + timestamp: string | undefined; + }; + if (!data) { + return; + } + if (!data.timestamp && !data.author) { + this.chat.length = 0; + } else { + this.chat = this.chat.filter((msg) => { + if (data.timestamp) { + return msg.id !== data.author || msg.timestamp !== data.timestamp; + } + return msg.id !== data.author; + }); + } + this.io.of(this.roomId).emit("chatinit", this.chat); + return; + }; +} + +function isValidUUID(id: string) { + return /^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test( + id, + ); +} diff --git a/server/server.ts b/server/server.ts new file mode 100644 index 0000000..9041ea9 --- /dev/null +++ b/server/server.ts @@ -0,0 +1,793 @@ +import config from "./config.ts"; +import fs from "node:fs"; +import express, { type Response } from "express"; +import bodyParser from "body-parser"; +import compression from "compression"; +import cors from "cors"; +import https from "node:https"; +import http from "node:http"; +import { Server } from "socket.io"; +import { searchYoutube, youtubePlaylist } from "./utils/youtube.ts"; +import { Room } from "./room.ts"; +import { redis, redisCount } from "./utils/redis.ts"; +import { + getCustomerByEmail, + createSelfServicePortal, + getIsSubscriberByEmail, +} from "./utils/stripe.ts"; +import { deleteUser, validateUserToken } from "./utils/firebase.ts"; +import path from "node:path"; +import { getStartOfDay } from "./utils/time.ts"; +import { getSessionLimitSeconds } from "./vm/utils.ts"; +import { postgres, insertObject, upsertObject } from "./utils/postgres.ts"; +import axios, { isAxiosError } from "axios"; +import crypto from "node:crypto"; +import { gzipSync } from "node:zlib"; +import { resolveShard } from "./utils/resolveShard.ts"; +import { makeRoomName, makeUserName } from "./utils/moniker.ts"; +import { getStats } from "./utils/getStats.ts"; + +if (process.env.NODE_ENV === "development") { + axios.interceptors.request.use( + (config) => { + // console.log(config); + return config; + }, + (error) => { + console.error(error); + }, + ); +} + +const releaseInterval = 5 * 60 * 1000; +const app = express(); +let server = null as https.Server | http.Server | null; +if (config.SSL_KEY_FILE && config.SSL_CRT_FILE) { + const key = fs.readFileSync(config.SSL_KEY_FILE); + const cert = fs.readFileSync(config.SSL_CRT_FILE); + server = https.createServer({ key: key, cert: cert }, app); +} else { + server = new http.Server(app); +} +server?.listen(config.PORT, config.HOST); + +const io = new Server(server, { cors: {}, transports: ["websocket"] }); +io.engine.use(async (req: any, res: Response, next: () => void) => { + const roomId = req._query.roomId; + if (!roomId) { + return next(); + } + // Attempt to ensure the room being connected to is loaded in memory + // If it doesn't exist, we may fail later with "invalid namespace" + const shard = resolveShard(roomId); + const key = "/" + roomId; + // Check to make sure this shard should load this room + const isCorrectShard = !config.SHARD || shard === Number(config.SHARD); + // Get the room data from postgres + const persistedRoom = ( + await postgres?.query( + `SELECT * from room where "roomId" = $1`, + [key], + ) + )?.rows?.[0]; + // Don't await after this because we may have a race condition where 2 rquests both try to load the room + if (isCorrectShard && !rooms.has(key)) { + const data = persistedRoom?.data + ? JSON.stringify(persistedRoom.data) + : undefined; + if (data) { + const room = new Room(io, key, data); + rooms.set(key, room); + console.log( + "loading room %s into memory on shard %s", + roomId, + config.SHARD, + ); + } + } + next(); +}); + +const rooms = new Map(); +// Following functions iterate over in-memory rooms +setInterval(minuteMetrics, 60 * 1000); +setInterval(release, releaseInterval); +setInterval(saveRooms, 1000); +if (process.env.NODE_ENV === "development") { + try { + import("./vmWorker.ts"); + // import('./syncSubs.ts'); + // import('./timeSeries.ts'); + } catch (e) { + console.error(e); + } +} + +app.use(cors()); +app.use(bodyParser.json()); +app.use(bodyParser.raw({ type: "text/plain", limit: 1000000 })); + +app.get("/ping", (_req, res) => { + res.json("pong"); +}); + +// Data's already compressed so go before the compression middleware +app.get("/subtitle/:hash", async (req, res) => { + const key = "subtitle:" + req.params.hash; + const buf = await redis?.getBuffer(key); + if (!buf) { + res.status(404).end("not found"); + return; + } + await redis?.expire(key, 24 * 60 * 60); + res.setHeader("Content-Encoding", "gzip"); + res.end(buf); +}); + +app.use(compression()); + +app.post("/subtitle", async (req, res) => { + const data = req.body; + if (!redis) { + return; + } + // calculate hash, gzip and save to redis + const hash = crypto + .createHash("sha256") + .update(data, "utf8") + .digest() + .toString("hex"); + let gzipData = gzipSync(data); + await redis.setex("subtitle:" + hash, 24 * 60 * 60, gzipData); + redisCount("subUploads"); + res.json({ hash }); +}); + +app.get("/downloadSubtitles", async (req, res) => { + // Request the URL from OS + try { + const urlResp = await axios<{ link: string }>({ + url: "https://api.opensubtitles.com/api/v1/download", + method: "POST", + headers: { + "User-Agent": "watchparty v1", + "Api-Key": config.OPENSUBTITLES_KEY, + Accept: "application/json", + "Content-Type": "application/json", + // 'Authorization': 'Bearer ' + config.OPENSUBTITLES_KEY, + }, + data: { + file_id: req.query.file_id, + // sub_format: 'srt', + }, + }); + redisCount("subDownloadsOS"); + if (!redis) { + // Return the direct link to the user, will work for about 3 hours + res.json(urlResp.data); + return; + } + // Cache the contents in Redis (longer retention) + const subResp = await axios.get(urlResp.data.link, { + responseType: "arraybuffer", + }); + const data = subResp.data; + const hash = crypto + .createHash("sha256") + .update(data, "utf8") + .digest() + .toString("hex"); + let gzipData = gzipSync(data); + await redis.setex("subtitle:" + hash, 24 * 60 * 60, gzipData); + res.json({ link: "/subtitle/" + hash }); + } catch (e) { + if (isAxiosError(e)) { + console.log(e.response); + } + throw e; + } +}); + +app.get("/searchSubtitles", async (req, res) => { + try { + const title = req.query.title ? String(req.query.title) : ""; + const url = req.query.url ? String(req.query.url) : ""; + let subUrl = ""; + if (url) { + const startResp = await axios({ + method: "get", + url: url, + headers: { + Range: "bytes=0-65535", + }, + responseType: "arraybuffer", + }); + const start = startResp.data; + const size = Number(startResp.headers["content-range"].split("/")[1]); + const endResp = await axios({ + method: "get", + url: url, + headers: { + Range: `bytes=${size - 65536}-`, + }, + responseType: "arraybuffer", + }); + const end = endResp.data; + // console.log(start, end, size); + let hash = computeOpenSubtitlesHash(start, end, size); + // hash = 'f65334e75574f00f'; + // Search API for subtitles by hash + subUrl = `https://api.opensubtitles.com/api/v1/subtitles?moviehash=${hash}&languages=en`; + } else if (title) { + subUrl = `https://api.opensubtitles.com/api/v1/subtitles?query=${title}&languages=en`; + } + // Alternative, web client calls this to get back some JS with the download URL embedded + // https://www.opensubtitles.com/nocache/download/7585196/subreq.js?file_name=Borgen.S04E01.en&locale=en&np=true&sub_frmt=srt&subtitle_id=6615808&ext_installed=false + // Up to 10 downloads per IP per day, but proxyable and doesn't require key + const response = await axios.get(subUrl, { + headers: { + "User-Agent": "watchparty v1", + "Api-Key": config.OPENSUBTITLES_KEY, + }, + }); + // console.log(subUrl, response.data); + const subtitles = response.data; + res.json(subtitles.data); + } catch (e: any) { + console.error(e.message); + res.json([]); + } + redisCount("subSearchesOS"); +}); + +app.get("/stats", async (req, res) => { + if (req.query.key && req.query.key === config.STATS_KEY) { + const stats = await getStats(); + res.json(stats); + } else { + res.status(403).json({ error: "Access Denied" }); + } +}); + +app.get("/health/:metric", async (req, res) => { + const vmManagerStats = ( + await axios.get("http://localhost:" + config.VMWORKER_PORT + "/stats") + ).data; + const result = vmManagerStats[req.params.metric]?.availableVBrowsers?.length; + res.status(result ? 200 : 500).json(result); +}); + +app.get("/timeSeries", async (req, res) => { + if (req.query.key && req.query.key === config.STATS_KEY && redis) { + const timeSeriesData = await redis.lrange("timeSeries", 0, -1); + const timeSeries = timeSeriesData.map((entry) => JSON.parse(entry)); + res.json(timeSeries); + } else { + res.status(403).json({ error: "Access Denied" }); + } +}); + +app.get("/youtube", async (req, res) => { + if (typeof req.query.q === "string") { + try { + redisCount("youtubeSearch"); + const items = await searchYoutube(req.query.q); + res.json(items); + } catch { + res.status(500).json({ error: "youtube error" }); + } + } else { + res.status(500).json({ error: "query must be a string" }); + } +}); + +app.get("/youtubePlaylist/:playlistId", async (req, res) => { + try { + const items = await youtubePlaylist(req.params.playlistId); + res.json(items); + } catch { + res.status(500).json({ error: "youtube error" }); + } +}); + +app.post("/createRoom", async (req, res) => { + const genName = () => "/" + makeRoomName(config.SHARD); + let name = genName(); + console.log("createRoom: ", name); + const newRoom = new Room(io, name); + if (postgres) { + const now = new Date(); + const roomObj = { + roomId: newRoom.roomId, + lastUpdateTime: now, + creationTime: now, + }; + try { + await insertObject(postgres, "room", roomObj); + } catch (e) { + redisCount("createRoomError"); + throw e; + } + } + const decoded = await validateUserToken(req.body?.uid, req.body?.token); + newRoom.creator = decoded?.email; + const preload = (req.body?.video || "").slice(0, 20000); + if (preload) { + redisCount("createRoomPreload"); + newRoom.video = preload; + newRoom.paused = true; + await newRoom.saveRoom(); + } + const prePlaylist = Array.isArray(req.body?.playlist) && req.body?.playlist; + if (prePlaylist) { + for (let item of req.body.playlist) { + newRoom.playlistAdd(null, item); + } + } + rooms.set(name, newRoom); + res.json({ name }); +}); + +app.post("/manageSub", async (req, res) => { + const decoded = await validateUserToken( + String(req.body?.uid), + String(req.body?.token), + ); + if (!decoded) { + res.status(400).json({ error: "invalid user token" }); + return; + } + if (!decoded.email) { + res.status(400).json({ error: "no email found" }); + return; + } + const customer = await getCustomerByEmail(decoded.email); + if (!customer) { + res.status(400).json({ error: "customer not found" }); + return; + } + const session = await createSelfServicePortal( + customer.id, + req.body?.return_url, + ); + res.json(session); +}); + +app.delete("/deleteAccount", async (req, res) => { + // TODO pass this in req.query instead + const decoded = await validateUserToken(req.body?.uid, req.body?.token); + if (!decoded) { + res.status(400).json({ error: "invalid user token" }); + return; + } + if (postgres) { + // Delete rooms + await postgres.query("DELETE FROM room WHERE owner = $1", [decoded.uid]); + // Delete linked accounts + await postgres.query("DELETE FROM link_account WHERE uid = $1", [ + decoded.uid, + ]); + } + await deleteUser(decoded.uid); + redisCount("deleteAccount"); + res.json({}); +}); + +app.get("/metadata", async (req, res) => { + const decoded = await validateUserToken( + String(req.query?.uid), + String(req.query?.token), + ); + let isSubscriber = await getIsSubscriberByEmail(decoded?.email); + // Has the user ever been a subscriber? + // const customer = await getCustomerByEmail(decoded.email); + let isFreePoolFull = false; + try { + isFreePoolFull = ( + await axios.get( + "http://localhost:" + config.VMWORKER_PORT + "/isFreePoolFull", + ) + ).data.isFull; + } catch (e: any) { + console.warn("[WARNING]: free pool check failed: %s", e.code); + } + const beta = + decoded?.email != null && + Boolean(config.BETA_USER_EMAILS.split(",").includes(decoded?.email)); + const streamPath = beta ? config.STREAM_PATH : undefined; + const convertPath = isSubscriber ? config.CONVERT_PATH : undefined; + // log metrics but don't wait for it + if (postgres && decoded?.uid) { + upsertObject( + postgres, + "active_user", + { uid: decoded?.uid, lastActiveTime: new Date() }, + { uid: true }, + ); + } + res.json({ + isSubscriber, + isFreePoolFull, + beta, + streamPath, + convertPath, + }); +}); + +app.get("/resolveRoom/:vanity", async (req, res) => { + const vanity = req.params.vanity; + const result = await postgres?.query( + `SELECT "roomId", vanity from room WHERE LOWER(vanity) = $1`, + [vanity?.toLowerCase() ?? ""], + ); + // console.log(vanity, result.rows); + // We also use this for checking name availability, so just return null if it doesn't exist (http 200) + res.json(result?.rows[0] ?? null); +}); + +app.get("/roomData/:roomId", async (req, res) => { + // Returns the room data given a room ID + // Only return data if the room doesn't have a password + // If it does, we could accept it as a URL parameter but for now just don't support + const result = await postgres?.query( + `SELECT data from room WHERE "roomId" = $1 and password IS NULL`, + ["/" + req.params.roomId], + ); + res.json(result?.rows[0]?.data); +}); + +app.get("/resolveShard/:roomId", async (req, res) => { + const shardNum = resolveShard(req.params.roomId); + res.send(String(config.SHARD ? shardNum : "")); +}); + +app.get("/listRooms", async (req, res) => { + const decoded = await validateUserToken( + String(req.query?.uid), + String(req.query?.token), + ); + if (!decoded) { + res.status(400).json({ error: "invalid user token" }); + return; + } + const result = await postgres?.query( + `SELECT "roomId", vanity, password from room WHERE owner = $1`, + [decoded.uid], + ); + res.json(result?.rows ?? []); +}); + +app.delete("/deleteRoom", async (req, res) => { + const decoded = await validateUserToken( + String(req.query?.uid), + String(req.query?.token), + ); + if (!decoded) { + res.status(400).json({ error: "invalid user token" }); + return; + } + const result = await postgres?.query( + `DELETE from room WHERE owner = $1 and "roomId" = $2`, + [decoded.uid, req.query.roomId], + ); + res.json(result?.rows); +}); + +app.get("/linkAccount", async (req, res) => { + const decoded = await validateUserToken( + String(req.query?.uid), + String(req.query?.token), + ); + if (!decoded) { + res.status(400).json({ error: "invalid user token" }); + return; + } + if (!postgres) { + res.status(400).json({ error: "invalid database client" }); + return; + } + // Get the linked accounts for the user + let linkAccounts: LinkAccount[] = []; + if (decoded?.uid && postgres) { + const { rows } = await postgres.query( + "SELECT kind, accountid, accountname, discriminator FROM link_account WHERE uid = $1", + [decoded?.uid], + ); + linkAccounts = rows; + } + res.json(linkAccounts); +}); + +app.post("/linkAccount", async (req, res) => { + const decoded = await validateUserToken( + String(req.body?.uid), + String(req.body?.token), + ); + if (!decoded) { + res.status(400).json({ error: "invalid user token" }); + return; + } + if (!postgres) { + res.status(400).json({ error: "invalid database client" }); + return; + } + const kind = req.body?.kind; + if (kind === "discord") { + const tokenType = req.body?.tokenType; + const accessToken = req.body.accessToken; + // Get the token and verify the user + const response = await axios.get("https://discord.com/api/users/@me", { + headers: { + authorization: `${tokenType} ${accessToken}`, + }, + }); + const accountid = response.data.id; + const accountname = response.data.username; + const discriminator = response.data.discriminator; + // Store the user id, username, discriminator + await upsertObject( + postgres, + "link_account", + { + accountid: accountid, + accountname: accountname, + discriminator: discriminator, + uid: decoded.uid, + kind: kind, + }, + { uid: true, kind: true }, + ); + res.json({}); + } else { + res.status(400).json({ error: "unsupported kind" }); + } +}); + +app.delete("/linkAccount", async (req, res) => { + // TODO read from req.query instead + const decoded = await validateUserToken( + String(req.body?.uid), + String(req.body?.token), + ); + if (!decoded) { + res.status(400).json({ error: "invalid user token" }); + return; + } + if (!postgres) { + res.status(400).json({ error: "invalid database client" }); + return; + } + await postgres.query( + "DELETE FROM link_account WHERE uid = $1 AND kind = $2", + [decoded.uid, req.body.kind], + ); + res.json({}); +}); + +app.get("/generateName", async (req, res) => { + res.send(makeUserName()); +}); + +// Proxy video segments +app.get("/proxy/*splat", async (req, res) => { + redisCount("proxyReqs"); + try { + const parsed = new URL("http://localhost" + req.url); + const pathname = parsed.pathname.slice("/proxy".length); + const host = parsed.searchParams.get("host"); + if (pathname.endsWith("index-dvr.m3u8")) { + // VOD + // https://d2vjef5jvl6bfs.cloudfront.net/3012391a6c3e84c79ef6_gamesdonequick_41198403369_1681059003/chunked/index-dvr.m3u8 + const resp = await axios.get("https://" + host + pathname); + const re2 = /(.*.ts)/g; + let repl = resp.data.replaceAll(re2, `$1?host=${host}`); + // Mark this as a VOD + repl += "#EXT-X-ENDLIST"; + res.send(repl); + } else if (pathname.endsWith(".m3u8")) { + // Stream + // https://video-weaver.sea02.hls.ttvnw.net/v1/playlist/CrQEgv7Mz6nnsfJH3XtVQxeYXk8mViy1zNGWglcybvxZsI1rv3iLnjAnnqwCiVXCJ-DdD27J6RuFrLy7YUYwHUCKazIKICIupUCn9UXtaBYhBM5JIYqg9dz6NWYrCWU9HZJj2TGROv9mAOKuTR51YS82hdYL4PFZa3xxWXhgDsxXQHNDB03kY6S0aG0-EVva1xYrn5Ge6IAXRwug9QDGlb-ydtF3BtYppoTklVI7CVLySPPwbbt5Ow1JXdnKhLSwQEs4bh3BLwMnRBwUFI5nmE18BLYbkMOUivgYP5SSMgnGGlSkJO-iJNPWvepunEgyBUzB_7L-b1keTcV-Qak9IcWIITIWbRvmg6qB3ZSuWdcJgWKmdXdIn4qoRM4o16G1_0N_WRqPtMQFo0hmTlAVmHrzRArJQmaSgqAxZxRbFMd9RFeX6qjP9NtwguPbSeStdVbQxMNC34iavYUIxo8Ug812BHsG7J_kIlof2zkIqkEbP3oV3UkSByIo7xh9EEVargjaGDuQRt8zPQ6-fNBWJJe9F6IFu7lXBPIJ016lopyfcvTWjbLbBHsVkg6vG-3UISh0nud7KB5g5ipQePhtcFSI5hvjlfX1DAVHEpTWXkvlnL4wNqEqpBYL2btSXYeE1Cb-RAvrAT0s61usERcL2eI-S5aTcSO8_hxQ2afC7c9vlypOWgP6p6XNpViZHXmdXv4t-d68Z-MpLtSU7VbB3pRWnSswFFyA3W39ITic4lb97Djp3wHhGgz0Sy8aDb9r0tnphIYgASoJdXMtZWFzdC0yMKQG.m3u8 + // Extract the edge URL host and add it to URL so proxy can fetch + const resp = await axios.get("https://" + host + pathname); + // const re = /https:\/\/(.*)\/v1\/segment\/(.*)/g; + // const match = re.exec(resp.data); + // const edgehost = match?.[1]; + // const repl = resp.data.replaceAll( + // re, + // `/proxy/v1/segment/$2?host=${edgehost}`, + // ); + const repl = resp.data; + res.send(repl); + } else if (pathname.endsWith(".ts")) { + // Segment + const resp = await axios.get("https://" + host + pathname, { + responseType: "arraybuffer", + }); + res.writeHead(200, { + "Content-Type": "application/octet-stream", + "Accept-Ranges": "bytes", + "Content-Length": resp.data.length, + "Transfer-Encoding": "chunked", + }); + res.write(resp.data); + res.end(); + } else { + res.status(404); + res.end(); + } + } catch (e) { + // console.log(e); + console.log("proxy failed: %s", req.url); + } +}); + +app.use(express.static(config.BUILD_DIRECTORY)); +// Send index.html for all other requests (SPA) +app.use("/*splat", (_req, res) => { + res.sendFile( + path.resolve( + import.meta.dirname + `/../${config.BUILD_DIRECTORY}/index.html`, + ), + ); +}); + +async function saveRooms() { + // Unload rooms that are empty and idle + // Frees up some JS memory space when process is long-running + // On reconnect, we'll attempt to reload the room + let saveCount = 0; + let skipCount = 0; + const start = Date.now(); + await Promise.all( + Array.from(rooms.entries()).map(async ([key, room]) => { + if ( + room.roster.length === 0 && + !room.vBrowser && + Number(room.lastUpdateTime) < Date.now() - 8 * 60 * 60 * 1000 + ) { + console.log( + "freeing room %s from memory on shard %s", + key, + config.SHARD, + ); + await room.saveRoom(); + room.destroy(); + rooms.delete(key); + saveCount += 1; + // Unregister the namespace to avoid dupes on reload + io._nsps.delete(key); + } else if (room.roster.length) { + room.lastUpdateTime = new Date(); + await room.saveRoom(); + saveCount += 1; + } else { + skipCount += 1; + } + }), + ); + const end = Date.now(); + console.log( + "[SAVEROOMS] %s saved in %sms, %s skipped", + saveCount, + end - start, + skipCount, + ); +} + +async function release() { + // Reset VMs in rooms that are: + // older than the session limit + // assigned to a room with no users + const roomArr = Array.from(rooms.values()); + console.log("[RELEASE] %s rooms in batch", roomArr.length); + for (let room of roomArr) { + if (room.vBrowser && room.vBrowser.assignTime) { + const maxTime = getSessionLimitSeconds(room.vBrowser.large) * 1000; + const elapsed = Date.now() - room.vBrowser.assignTime; + const ttl = maxTime - elapsed; + const isTimedOut = ttl && ttl < releaseInterval; + const isAlmostTimedOut = ttl && ttl < releaseInterval * 2; + const isRoomEmpty = room.roster.length === 0; + const isRoomIdle = + Date.now() - Number(room.lastUpdateTime) > 5 * 60 * 1000; + if (isTimedOut || (isRoomEmpty && isRoomIdle)) { + console.log("[RELEASE] VM in room:", room.roomId); + room.stopVBrowserInternal(); + if (isTimedOut) { + room.addChatMessage(null, { + id: "", + system: true, + cmd: "vBrowserTimeout", + msg: "", + }); + redisCount("vBrowserTerminateTimeout"); + } else if (isRoomEmpty) { + redisCount("vBrowserTerminateEmpty"); + } + } else if (isAlmostTimedOut) { + room.addChatMessage(null, { + id: "", + system: true, + cmd: "vBrowserAlmostTimeout", + msg: "", + }); + } + } + // We want to spread out the jobs over about half the release interval + // This gives other jobs some CPU time + const waitTime = releaseInterval / 2 / roomArr.length; + await new Promise((resolve) => setTimeout(resolve, waitTime)); + } +} + +async function minuteMetrics() { + const roomArr = Array.from(rooms.values()); + let vbWaiting = 0; + for (let room of roomArr) { + if (room.vBrowser && room.vBrowser.id) { + // Update the heartbeat + await postgres?.query( + `UPDATE vbrowser SET "heartbeatTime" = NOW() WHERE "roomId" = $1 and vmid = $2`, + [room.roomId, room.vBrowser.id], + ); + + const expireTime = getStartOfDay() / 1000 + 86400; + if (room.vBrowser?.creatorClientID) { + await redis?.zincrby( + "vBrowserClientIDMinutes", + 1, + room.vBrowser.creatorClientID, + ); + await redis?.expireat("vBrowserClientIDMinutes", expireTime); + } + if (room.vBrowser?.creatorUID) { + await redis?.zincrby( + "vBrowserUIDMinutes", + 1, + room.vBrowser?.creatorUID, + ); + await redis?.expireat("vBrowserUIDMinutes", expireTime); + } + } + const users = room.roster.length; + if (users) { + await redis?.setex(`roomCounts:${room.roomId}`, 120, users); + await redis?.setex( + `roomRosters:${room.roomId}`, + 120, + JSON.stringify(room.getRosterForStats()), + ); + } + vbWaiting += room.vBrowserQueue ? 1 : 0; + } + // Report shard metrics + const obj: ShardMetric = { + uptime: process.uptime(), + mem: process.memoryUsage().rss, + roomCount: rooms.size, + users: io.engine.clientsCount, + vbWaiting, + }; + await redis?.setex( + `shardMetrics:${config.SHARD ?? 0}`, + 120, + JSON.stringify(obj), + ); +} + +function computeOpenSubtitlesHash(first: Buffer, last: Buffer, size: number) { + // console.log(first.length, last.length, size); + let temp = BigInt(size); + process(first); + process(last); + + temp = temp & BigInt("0xffffffffffffffff"); + return temp.toString(16).padStart(16, "0"); + + function process(chunk: Buffer) { + for (let i = 0; i < chunk.length; i += 8) { + const long = chunk.readBigUInt64LE(i); + temp += long; + } + } +} diff --git a/server/syncSubs.ts b/server/syncSubs.ts new file mode 100644 index 0000000..f9cd247 --- /dev/null +++ b/server/syncSubs.ts @@ -0,0 +1,156 @@ +import config from "./config.ts"; +import { getUserByEmail } from "./utils/firebase.ts"; +import { insertObject, newPostgres, updateObject } from "./utils/postgres.ts"; +import { getAllActiveSubscriptions, getAllCustomers } from "./utils/stripe.ts"; +import { Client as DiscordClient, IntentsBitField } from "discord.js"; + +let lastSubs = ""; +let currentSubs = ""; + +const postgres2 = newPostgres(); + +// set up the Discord admin bot +const discordBot = new DiscordClient({ + intents: [IntentsBitField.Flags.Guilds, IntentsBitField.Flags.GuildMembers], +}); +if (config.DISCORD_ADMIN_BOT_TOKEN) { + discordBot.login(config.DISCORD_ADMIN_BOT_TOKEN); + // discordBot.once('ready', () => { + // console.log(`Discord Bot "${discordBot?.user?.username}" ready`); + // }); +} +if (process.env.NODE_ENV === "development") { + setTimeout(syncSubscribers, 1000); +} + +setInterval(syncSubscribers, 60 * 1000); + +async function syncSubscribers() { + if (!config.STRIPE_SECRET_KEY || !config.FIREBASE_ADMIN_SDK_CONFIG) { + return; + } + console.time("syncSubscribers"); + // Fetch subs, customers from stripe + const [subs, customers] = await Promise.all([ + getAllActiveSubscriptions(), + getAllCustomers(), + ]); + + const emailMap = new Map(); + customers.forEach((cust) => { + emailMap.set(cust.id, cust.email); + }); + + console.log("%s subs in Stripe", subs.length); + + const uidMap = new Map(); + for (let i = 0; i < subs.length; i += 50) { + // Batch customers and fetch firebase data + const batch = subs.slice(i, i + 50); + const fbUsers = await Promise.all( + batch + .map((sub) => + emailMap.get(sub.customer) + ? getUserByEmail(emailMap.get(sub.customer)) + : null, + ) + .filter(Boolean), + ); + fbUsers.forEach((user) => { + uidMap.set(user?.email, user?.uid); + }); + } + + let noUID = 0; + // Create sub objects + let result = subs + .map((sub) => { + let uid = uidMap.get(emailMap.get(sub.customer)); + if (!uid) { + uid = emailMap.get(sub.customer); + noUID += 1; + } + return { + customerId: sub.customer, + email: emailMap.get(sub.customer), + status: sub.status, + uid, + }; + }) + .filter((sub) => sub.uid); + console.log("%s subs to insert", result.length); + console.log("%s subs do not have UID, using email", noUID); + + const newResult = result.filter( + (sub, index) => + index === result.findIndex((other) => sub.uid === other.uid), + ); + console.log("%s deduped subs to insert", newResult.length); + if (result.length !== newResult.length) { + // Log the difference + console.log(result.filter((x) => !newResult.includes(x))); + } + result = newResult; + + currentSubs = result + .map((sub) => sub.uid) + .sort() + .join(); + + // Upsert to DB + // console.log(result); + if (currentSubs !== lastSubs) { + try { + await postgres2?.query("BEGIN TRANSACTION"); + await postgres2?.query("DELETE FROM subscriber"); + await postgres2?.query('UPDATE room SET "isSubRoom" = false'); + for (let row of result) { + await insertObject(postgres2, "subscriber", row); + await updateObject( + postgres2, + "room", + { isSubRoom: true }, + { owner: row.uid }, + ); + } + await postgres2?.query("COMMIT"); + lastSubs = currentSubs; + } catch (e) { + console.error(e); + await postgres2?.query("ROLLBACK"); + } + } + if ( + discordBot.isReady() && + config.DISCORD_ADMIN_BOT_SERVER_ID && + config.DISCORD_ADMIN_BOT_SUB_ROLE_ID + ) { + console.log("setting discord roles"); + // Update the sub status of users in Discord + // Join the current subs with linked accounts + const guild = discordBot.guilds.cache.get( + config.DISCORD_ADMIN_BOT_SERVER_ID, + ); + const role = guild?.roles.cache.get(config.DISCORD_ADMIN_BOT_SUB_ROLE_ID); + const toUpdate = ( + await postgres2.query( + `SELECT la.accountid from subscriber JOIN link_account la ON subscriber.uid = la.uid WHERE la.kind = 'discord'`, + ) + ).rows; + console.log("%s users to set sub role", toUpdate.length); + for (let row of toUpdate) { + try { + const user = await guild?.members.fetch(row.accountid); + if (user && role) { + console.log("assigning role %s to user %s", role, user.id); + + await user.roles.add(role); + } + } catch (e: any) { + console.log(e.message); + } + } + } + + console.timeEnd("syncSubscribers"); +} diff --git a/server/timeSeries.ts b/server/timeSeries.ts new file mode 100644 index 0000000..7be0f1f --- /dev/null +++ b/server/timeSeries.ts @@ -0,0 +1,53 @@ +import config from "./config.ts"; +import axios from "axios"; +import { redis } from "./utils/redis.ts"; +import { getStats } from "./utils/getStats.ts"; + +statsTimeSeries(); +setInterval(statsTimeSeries, 5 * 60 * 1000); + +async function statsTimeSeries() { + if (redis) { + console.time("timeSeries"); + try { + const stats = await getStats(); + const isFreePoolFull = ( + await axios.get( + "http://localhost:" + config.VMWORKER_PORT + "/isFreePoolFull", + ) + ).data.isFull; + const datapoint: AnyDict = { + time: new Date(), + currentUsers: stats.counts.currentUsers, + currentVBrowser: stats.counts.currentVBrowser, + currentVBrowserLarge: stats.counts.currentVBrowserLarge, + currentHttp: stats.counts.currentHttp, + currentScreenShare: stats.counts.currentScreenShare, + currentFileShare: stats.counts.currentFileShare, + currentVideoChat: stats.counts.currentVideoChat, + chatMessages: stats.counts.chatMessages, + redisUsage: stats.counts.redisUsage, + hetznerApiRemaining: stats.counts.hetznerApiRemaining, + avgStartMS: + (stats.vBrowserStartMS || []).map(Number).reduce((a, b) => a + b, 0) / + (stats.vBrowserStartMS?.length ?? 0), + vBrowserStarts: stats.counts.vBrowserStarts, + vBrowserLaunches: stats.counts.vBrowserLaunches, + vBrowserFails: stats.counts.vBrowserFails, + vBrowserStagingFails: stats.counts.vBrowserStagingFails, + isFreePoolFull: Number(isFreePoolFull), + }; + Object.keys(stats.vmManagerStats).forEach((key) => { + if (stats.vmManagerStats[key]) { + datapoint[key] = + stats.vmManagerStats[key]?.availableVBrowsers?.length; + } + }); + await redis.lpush("timeSeries", JSON.stringify(datapoint)); + await redis.ltrim("timeSeries", 0, 288); + } catch (e: any) { + console.warn(`[TIMESERIES] %s when collecting stats`, e.code); + } + console.timeEnd("timeSeries"); + } +} diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000..be5160f --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "nodenext", + "strict": true, + "noEmit": true, + "allowJs": true, + "skipLibCheck": true, + "allowImportingTsExtensions": true, + "erasableSyntaxOnly": true, + "verbatimModuleSyntax": true + }, + "include": [".", "../global.d.ts"] +} diff --git a/server/utils/firebase.ts b/server/utils/firebase.ts new file mode 100644 index 0000000..d5e2e29 --- /dev/null +++ b/server/utils/firebase.ts @@ -0,0 +1,73 @@ +import config from "../config.ts"; +import admin from "firebase-admin"; + +if (config.FIREBASE_ADMIN_SDK_CONFIG) { + admin.initializeApp({ + credential: admin.credential.cert( + JSON.parse(config.FIREBASE_ADMIN_SDK_CONFIG), + ), + databaseURL: config.FIREBASE_DATABASE_URL, + }); +} + +export async function validateUserToken(uid: string, token: string) { + if (!config.FIREBASE_ADMIN_SDK_CONFIG) { + return undefined; + } + if (!token) { + return undefined; + } + try { + const decoded = await admin.auth().verifyIdToken(token); + if (uid !== decoded.uid) { + // Valid but for wrong user + return undefined; + } + return decoded; + } catch (e) { + // Promise rejects if verification failed + console.log(e); + return undefined; + } +} + +export async function writeData(key: string, value: string) { + if (!config.FIREBASE_ADMIN_SDK_CONFIG) { + return; + } + await admin.database().ref(key).set(value); +} + +export async function getUserByEmail(email: string) { + if (!config.FIREBASE_ADMIN_SDK_CONFIG) { + return null; + } + try { + return await admin.auth().getUserByEmail(email); + } catch (e: any) { + console.log(email, e.message); + } + return null; +} + +export async function getUser(uid: string) { + if (!config.FIREBASE_ADMIN_SDK_CONFIG) { + return null; + } + return await admin.auth().getUser(uid); +} + +export async function getUserEmail(uid: string) { + if (!config.FIREBASE_ADMIN_SDK_CONFIG) { + return null; + } + const user = await admin.auth().getUser(uid); + return user.email; +} + +export async function deleteUser(uid: string) { + if (!config.FIREBASE_ADMIN_SDK_CONFIG) { + return null; + } + return admin.auth().deleteUser(uid); +} diff --git a/server/utils/getStats.ts b/server/utils/getStats.ts new file mode 100644 index 0000000..540f255 --- /dev/null +++ b/server/utils/getStats.ts @@ -0,0 +1,312 @@ +import type { AssignedVM } from "../vm/base.ts"; +import { postgres } from "./postgres.ts"; +import os from "node:os"; +import { getRedisCountDay, getRedisCountDayDistinct, redis } from "./redis.ts"; +import config from "../config.ts"; +import { apps } from "../ecosystem.config.js"; + +export async function getStats() { + const now = Date.now(); + + let currentUsers = 0; + + // Render each shard metrics as its own object + const shardMetrics: Record = {}; + const shardKeys = new Set( + apps.map((app) => `shardMetrics:${app.env?.SHARD ?? 0}`), + ); + for (let key of shardKeys) { + const resp2 = await redis?.get(key); + if (resp2) { + shardMetrics[key] = JSON.parse(resp2); + currentUsers += shardMetrics[key].users; + } + } + + // Count these from postgres data + let currentHttp = 0; + let currentVBrowser = 0; + let currentVBrowserLarge = 0; + let currentScreenShare = 0; + let currentFileShare = 0; + const currentRoomSizes: Record = {}; + + const result = await postgres?.query<{ + roomId: string; + creationTime: Date; + lastUpdateTime: Date; + vanity: string; + isSubRoom: boolean; + roomTitle: string; + roomDescription: string; + mediaPath: string; + owner: string; + password: string; + video: string; + videoTS: number; + vBrowser: AssignedVM; + creator: string; + lock: string; + }>( + `SELECT "roomId", "creationTime", "lastUpdateTime", vanity, "isSubRoom", "roomTitle", "roomDescription", "mediaPath", owner, password, + data->'video' as video, data->'videoTS' as "videoTS", data->'vBrowser' as "vBrowser", data->'creator' as creator, data->'lock' as lock + FROM room + WHERE "lastUpdateTime" > NOW() - INTERVAL '7 day' + AND length(data->>'video') > 0 + ORDER BY "creationTime" DESC`, + ); + const currentRoomData = await Promise.all( + (result?.rows ?? []).map(async (dbRoom) => { + const vBrowser = dbRoom.vBrowser; + if (vBrowser) { + currentVBrowser += 1; + } + if (vBrowser?.large) { + currentVBrowserLarge += 1; + } + const rosterLength = Number( + await redis?.get(`roomCounts:${dbRoom.roomId}`), + ); + let roster = []; + if (rosterLength) { + currentRoomSizes[rosterLength] = + (currentRoomSizes[rosterLength] ?? 0) + 1; + const resp = await redis?.get(`roomRosters:${dbRoom.roomId}`); + if (resp) { + roster = JSON.parse(resp); + } + } + const obj = { + roomId: dbRoom.roomId, + video: dbRoom.video || undefined, + videoTS: dbRoom.videoTS || undefined, + creationTime: dbRoom.creationTime || undefined, + lastUpdateTime: dbRoom.lastUpdateTime || undefined, + vanity: dbRoom.vanity || undefined, + isSubRoom: dbRoom.isSubRoom || undefined, + owner: dbRoom.owner || undefined, + password: dbRoom.password || undefined, + roomTitle: dbRoom.roomTitle || undefined, + roomDescription: dbRoom.roomDescription || undefined, + mediaPath: dbRoom.mediaPath || undefined, + vBrowser, + vBrowserElapsed: vBrowser?.assignTime && now - vBrowser?.assignTime, + lock: dbRoom.lock || undefined, + creator: dbRoom.creator || undefined, + rosterLength, + roster, + }; + if (obj.video?.startsWith("http") && rosterLength) { + currentHttp += 1; + } + if (obj.video?.startsWith("screenshare://") && rosterLength) { + currentScreenShare += 1; + } + if (obj.video?.startsWith("fileshare://") && rosterLength) { + currentFileShare += 1; + } + return obj; + }), + ); + // Singleton stats below (same for all shards) + const currentVideoChat = 0; + const cpuUsage = os.loadavg()[1] * 100; + const redisUsage = Number( + (await redis?.info()) + ?.split("\n") + .find((line) => line.startsWith("used_memory:")) + ?.split(":")[1] + .trim(), + ); + const postgresUsage = Number( + (await postgres?.query(`SELECT pg_database_size('postgres');`))?.rows[0] + .pg_database_size, + ); + const numPermaRooms = Number( + (await postgres?.query("SELECT count(1) from room WHERE owner IS NOT NULL")) + ?.rows[0].count, + ); + const numAllRooms = Number( + (await postgres?.query("SELECT count(1) from room"))?.rows[0].count, + ); + const numSubs = Number( + (await postgres?.query("SELECT count(1) from subscriber"))?.rows[0].count, + ); + const discordBotWatch = await getRedisCountDay("discordBotWatch"); + const createRoomErrors = await getRedisCountDay("createRoomError"); + const deleteAccounts = await getRedisCountDay("deleteAccount"); + const chatMessages = await getRedisCountDay("chatMessages"); + const addReactions = await getRedisCountDay("addReaction"); + const hetznerApiRemaining = Number(await redis?.get("hetznerApiRemaining")); + const vBrowserStarts = await getRedisCountDay("vBrowserStarts"); + const vBrowserLaunches = await getRedisCountDay("vBrowserLaunches"); + const vBrowserFails = await getRedisCountDay("vBrowserFails"); + const vBrowserStagingFails = await getRedisCountDay("vBrowserStagingFails"); + const vBrowserReimages = await getRedisCountDay("vBrowserReimage"); + const vBrowserCleanups = await getRedisCountDay("vBrowserCleanup"); + const vBrowserStopTimeout = await getRedisCountDay( + "vBrowserTerminateTimeout", + ); + const vBrowserStopEmpty = await getRedisCountDay("vBrowserTerminateEmpty"); + const vBrowserStopManual = await getRedisCountDay("vBrowserTerminateManual"); + const vBrowserStartMS = await redis?.lrange("vBrowserStartMS", 0, -1); + const vBrowserStageRetries = await redis?.lrange( + "vBrowserStageRetries", + 0, + -1, + ); + const vBrowserStageFails = await redis?.lrange("vBrowserStageFails", 0, -1); + const vBrowserSessionMS = await redis?.lrange("vBrowserSessionMS", 0, -1); + // const vBrowserVMLifetime = await redis?.lrange('vBrowserVMLifetime', 0, -1); + const proxyReqs = await getRedisCountDay("proxyReqs"); + const urlStarts = await getRedisCountDay("urlStarts"); + const streamStarts = await getRedisCountDay("streamStarts"); + const convertStarts = await getRedisCountDay("convertStarts"); + const playlistAdds = await getRedisCountDay("playlistAdds"); + const screenShareStarts = await getRedisCountDay("screenShareStarts"); + const fileShareStarts = await getRedisCountDay("fileShareStarts"); + const mediasoupStarts = await getRedisCountDay("mediasoupStarts"); + const videoChatStarts = await getRedisCountDay("videoChatStarts"); + const connectStarts = await getRedisCountDay("connectStarts"); + const connectStartsDistinct = await getRedisCountDayDistinct( + "connectStartsDistinct", + ); + const subUploads = await getRedisCountDay("subUploads"); + const subDownloadsOS = await getRedisCountDay("subDownloadsOS"); + const subSearchesOS = await getRedisCountDay("subSearchesOS"); + const youtubeSearch = await getRedisCountDay("youtubeSearch"); + const vBrowserClientIDsCard = await redis?.zcard("vBrowserClientIDs"); + const vBrowserUIDsCard = await redis?.zcard("vBrowserUIDs"); + const createRoomPreloads = await getRedisCountDay("createRoomPreload"); + + const vBrowserClientIDs = altArrayToObject( + await redis?.zrevrangebyscore( + "vBrowserClientIDs", + "+inf", + "0", + "WITHSCORES", + "LIMIT", + 0, + 20, + ), + ); + const vBrowserUIDs = altArrayToObject( + await redis?.zrevrangebyscore( + "vBrowserUIDs", + "+inf", + "0", + "WITHSCORES", + "LIMIT", + 0, + 20, + ), + ); + const vBrowserClientIDMinutes = altArrayToObject( + await redis?.zrevrangebyscore( + "vBrowserClientIDMinutes", + "+inf", + "0", + "WITHSCORES", + "LIMIT", + 0, + 20, + ), + ); + const vBrowserUIDMinutes = altArrayToObject( + await redis?.zrevrangebyscore( + "vBrowserUIDMinutes", + "+inf", + "0", + "WITHSCORES", + "LIMIT", + 0, + 20, + ), + ); + + // Fetch VM stats from vmworker + const resp = await fetch( + "http://localhost:" + config.VMWORKER_PORT + "/stats", + ); + const vmManagerStats = await resp.json(); + + return { + ...shardMetrics, + currentRoomSizes, + counts: { + currentUsers, + currentVideoChat, + currentVBrowser, + currentVBrowserLarge, + currentHttp, + currentScreenShare, + currentFileShare, + cpuUsage, + redisUsage, + postgresUsage, + numPermaRooms, + numAllRooms, + numSubs, + discordBotWatch, + createRoomErrors, + createRoomPreloads, + deleteAccounts, + chatMessages, + addReactions, + proxyReqs, + urlStarts, + streamStarts, + convertStarts, + playlistAdds, + screenShareStarts, + fileShareStarts, + mediasoupStarts, + subUploads, + subDownloadsOS, + subSearchesOS, + youtubeSearch, + videoChatStarts, + connectStarts, + connectStartsDistinct, + hetznerApiRemaining, + vBrowserStarts, + vBrowserLaunches, + vBrowserFails, + vBrowserStagingFails, + vBrowserReimages, + vBrowserCleanups, + vBrowserStopManual, + vBrowserStopEmpty, + vBrowserStopTimeout, + vBrowserClientIDsCard, + vBrowserUIDsCard, + }, + // Stats object from vmWorker (render as JSON) + vmManagerStats, + // Array of room data (render as JSON) + currentRoomData, + // Arrays of last values (render as one column table) + vBrowserStartMS, + vBrowserStageRetries, + vBrowserStageFails, + vBrowserSessionMS, + // Maps of vbrowser users + vBrowserClientIDs, + vBrowserClientIDMinutes, + vBrowserUIDs, + vBrowserUIDMinutes, + }; +} + +function altArrayToObject(arr: string[] | undefined) { + const result: Record = {}; + if (!arr) { + return result; + } + for (let i = 0; i < arr.length; i += 2) { + const k = arr[i]; + const v = arr[i + 1]; + result[k] = Number(v); + } + return result; +} diff --git a/server/utils/moniker.ts b/server/utils/moniker.ts new file mode 100644 index 0000000..49c7fdc --- /dev/null +++ b/server/utils/moniker.ts @@ -0,0 +1,41 @@ +import fs from "node:fs"; +import { resolveShard } from "./resolveShard.ts"; + +let adjectives = fs + .readFileSync(process.cwd() + "/words/adjectives.txt") + .toString() + .split("\n"); +const nouns = fs + .readFileSync(process.cwd() + "/words/nouns.txt") + .toString() + .split("\n"); +const verbs = fs + .readFileSync(process.cwd() + "/words/verbs.txt") + .toString() + .split("\n"); +const randomElement = (array: string[]) => + array[Math.floor(Math.random() * array.length)]; + +export function makeRoomName(shard: number | undefined) { + let filteredAdjectives = adjectives; + if (shard) { + // Filter the adjective list by shard + filteredAdjectives = adjectives.filter( + (adj) => resolveShard(adj) === Number(shard), + ); + } + const adjective = randomElement(filteredAdjectives); + const noun = randomElement(nouns); + const verb = randomElement(verbs); + return `${adjective}-${noun}-${verb}`; +} + +export function makeUserName() { + return `${capFirst(randomElement(adjectives))} ${capFirst( + randomElement(nouns), + )}`; +} + +function capFirst(string: string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} diff --git a/server/utils/playlist.ts b/server/utils/playlist.ts new file mode 100644 index 0000000..8b7c6e7 --- /dev/null +++ b/server/utils/playlist.ts @@ -0,0 +1,7 @@ +export const findPlaylistVideoByUrl = ( + playlist: PlaylistVideo[], + url?: string, +) => { + if (!url) return; + return playlist.find((video) => video.url === url); +}; diff --git a/server/utils/postgres.ts b/server/utils/postgres.ts new file mode 100644 index 0000000..04b0020 --- /dev/null +++ b/server/utils/postgres.ts @@ -0,0 +1,87 @@ +import { Client, type QueryResult } from "pg"; +import config from "../config.ts"; + +export let postgres: Client | undefined = undefined; +if (config.DATABASE_URL) { + postgres = new Client({ + connectionString: config.DATABASE_URL, + ssl: { rejectUnauthorized: false }, + }); + postgres.connect(); +} + +/** + * Use this if we need a new connection instead of sharing. + * Guarantees we'll return a client because we throw if we don't have it configured + * @returns + */ +export function newPostgres() { + if (!config.DATABASE_URL) { + throw new Error("postgres not configured"); + } + const postgres = new Client({ + connectionString: config.DATABASE_URL, + ssl: { rejectUnauthorized: false }, + }); + postgres.connect(); + return postgres; +} + +export async function updateObject( + postgres: Client, + table: string, + object: AnyDict, + condition: AnyDict, +): Promise> { + const columns = Object.keys(object); + const values = Object.values(object); + // TODO support compound conditions, not just one + let query = `UPDATE ${table} SET ${columns + .map((c, i) => `"${c}" = $${i + 1}`) + .join(",")} + WHERE "${Object.keys(condition)[0]}" = $${Object.keys(object).length + 1} + RETURNING *`; + //console.log(query); + const result = await postgres.query(query, [ + ...values, + condition[Object.keys(condition)[0]], + ]); + return result; +} + +export async function insertObject( + postgres: Client, + table: string, + object: AnyDict, +): Promise> { + const columns = Object.keys(object); + const values = Object.values(object); + let query = `INSERT INTO ${table} (${columns.map((c) => `"${c}"`).join(",")}) + VALUES (${values.map((_, i) => "$" + (i + 1)).join(",")}) + RETURNING *`; + // console.log(query); + const result = await postgres.query(query, values); + return result; +} + +export async function upsertObject( + postgres: Client, + table: string, + object: AnyDict, + conflict: BooleanDict, +): Promise> { + const columns = Object.keys(object); + const values = Object.values(object); + let query = `INSERT INTO ${table} (${columns.map((c) => `"${c}"`).join(",")}) + VALUES (${values.map((_, i) => "$" + (i + 1)).join(",")}) + ON CONFLICT (${Object.keys(conflict) + .map((k) => `"${k}"`) + .join(",")}) + DO UPDATE SET ${Object.keys(object) + .map((c) => `"${c}" = EXCLUDED."${c}"`) + .join(",")} + RETURNING *`; + // console.log(query); + const result = await postgres.query(query, values); + return result; +} diff --git a/server/utils/redis.ts b/server/utils/redis.ts new file mode 100644 index 0000000..9e7863e --- /dev/null +++ b/server/utils/redis.ts @@ -0,0 +1,72 @@ +import config from "../config.ts"; +import { Redis } from "ioredis"; +import { getStartOfHour } from "./time.ts"; + +export let redis: Redis | undefined = undefined; +if (config.REDIS_URL) { + redis = new Redis(config.REDIS_URL); +} + +export async function redisCount(prefix: string) { + if (!redis) { + return; + } + try { + const key = `${prefix}:${getStartOfHour()}`; + await redis.incr(key); + await redis.expireat(key, getStartOfHour() + 86400 * 1000); + } catch (e) { + console.log(e); + } +} + +export async function getRedisCountDay(prefix: string) { + if (!redis) { + return; + } + // Get counts for last 24 hour keys (including current partial hour) + const keyArr = []; + for (let i = 0; i < 24; i += 1) { + keyArr.push(`${prefix}:${getStartOfHour() - i * 3600 * 1000}`); + } + const values = await redis.mget(...keyArr); + return values.reduce((a, b) => (Number(a) || 0) + (Number(b) || 0), 0); +} + +export async function getRedisCountHour(prefix: string) { + if (!redis) { + return; + } + // Get counts for previous full hour + const value = await redis.get(`${prefix}:${getStartOfHour() - 3600 * 1000}`); + return Number(value); +} + +export async function redisCountDistinct(prefix: string, item: string) { + if (!redis) { + return; + } + const key = `${prefix}:${getStartOfHour()}`; + await redis.pfadd(key, item); + await redis.expireat(key, getStartOfHour() + 86400 * 1000); +} + +export async function getRedisCountDayDistinct(prefix: string) { + if (!redis) { + return; + } + // Get counts for last 24 hour keys (including current partial hour) + const keyArr = []; + for (let i = 0; i < 24; i += 1) { + keyArr.push(`${prefix}:${getStartOfHour() - i * 3600 * 1000}`); + } + return await redis.pfcount(...keyArr); +} + +export async function getRedisCountHourDistinct(prefix: string) { + if (!redis) { + return; + } + // Get counts for previous full hour + return await redis.pfcount(`${prefix}:${getStartOfHour() - 3600 * 1000}`); +} diff --git a/server/utils/regex.ts b/server/utils/regex.ts new file mode 100644 index 0000000..109b550 --- /dev/null +++ b/server/utils/regex.ts @@ -0,0 +1,11 @@ +/** This regex searches for YouTube Video IDs from a YouTube URL */ +/** example: */ +/** YOUTUBE_VIDEO_ID_REGEX.exec('https://youtube.com/?v=14634524364) */ +/** will return the id 14634524364 in the first exec group [1] */ +export const YOUTUBE_VIDEO_ID_REGEX = + /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/; + +/** These regexes allow us to find hours, minutes and seconds from a ISO 8601 time string */ +export const PT_HOURS_REGEX = /(\d{1,2})H/; +export const PT_MINUTES_REGEX = /(\d{1,2})M/; +export const PT_SECONDS_REGEX = /(\d{1,2})S/; diff --git a/server/utils/resolveShard.ts b/server/utils/resolveShard.ts new file mode 100644 index 0000000..20ab8aa --- /dev/null +++ b/server/utils/resolveShard.ts @@ -0,0 +1,12 @@ +import config from "../config.ts"; +import { apps } from "../ecosystem.config.js"; + +export function resolveShard(roomId: string): number { + if (!config.SHARD) { + return 0; + } + const numShards = apps.filter((app) => app.env?.SHARD).length; + const letter = roomId[0]; + const charCode = letter.charCodeAt(0); + return Number((charCode % numShards) + 1); +} diff --git a/server/utils/stripe.ts b/server/utils/stripe.ts new file mode 100644 index 0000000..2c28555 --- /dev/null +++ b/server/utils/stripe.ts @@ -0,0 +1,61 @@ +import config from "../config.ts"; +import Stripe from "stripe"; + +const stripe = new Stripe(config.STRIPE_SECRET_KEY, { + apiVersion: "2020-08-27", +}); + +export async function getCustomerByEmail(email: string) { + if (!config.STRIPE_SECRET_KEY) { + return undefined; + } + const customer = await stripe.customers.list({ + email, + expand: ["data.subscriptions"], + }); + return customer?.data[0]; +} + +export async function getIsSubscriberByEmail(email: string | undefined) { + if (!config.STRIPE_SECRET_KEY) { + // If Stripe isn't set up assume everyone is a subscriber + return true; + } + if (!email) { + return false; + } + const customer = await getCustomerByEmail(email); + const isSubscriber = Boolean( + customer?.subscriptions?.data?.find((sub) => sub?.status === "active"), + ); + return isSubscriber; +} + +export async function createSelfServicePortal( + customerId: string, + returnUrl: string, +) { + return await stripe.billingPortal.sessions.create({ + customer: customerId, + return_url: returnUrl, + }); +} + +export async function getAllCustomers() { + const result = []; + for await (const customer of stripe.customers.list({ limit: 100 })) { + result.push(customer); + } + return result; +} + +export async function getAllActiveSubscriptions() { + const result = []; + for await (const sub of stripe.subscriptions.list({ + limit: 100, + status: "active", + })) { + result.push(sub); + } + return result; +} diff --git a/server/utils/time.ts b/server/utils/time.ts new file mode 100644 index 0000000..d18c4e9 --- /dev/null +++ b/server/utils/time.ts @@ -0,0 +1,14 @@ +export function getStartOfDay() { + const now = Date.now(); + return now - (now % 86400000); +} + +export function getStartOfHour() { + const now = Date.now(); + return now - (now % 3600000); +} + +export function getStartOfMinute() { + const now = Date.now(); + return now - (now % 60000); +} diff --git a/server/utils/youtube.ts b/server/utils/youtube.ts new file mode 100644 index 0000000..4256687 --- /dev/null +++ b/server/utils/youtube.ts @@ -0,0 +1,127 @@ +import config from "../config.ts"; +import { + PT_HOURS_REGEX, + PT_MINUTES_REGEX, + PT_SECONDS_REGEX, + YOUTUBE_VIDEO_ID_REGEX, +} from "./regex.ts"; +import { youtube, youtube_v3 } from "@googleapis/youtube"; + +let Youtube = config.YOUTUBE_API_KEY + ? youtube({ + version: "v3", + auth: config.YOUTUBE_API_KEY, + }) + : null; + +export const mapYoutubeSearchResult = ( + video: youtube_v3.Schema$SearchResult, +): PlaylistVideo => { + return { + channel: video.snippet?.channelTitle ?? "", + url: "https://www.youtube.com/watch?v=" + video?.id?.videoId, + name: video.snippet?.title ?? "", + img: video.snippet?.thumbnails?.default?.url ?? "", + duration: 0, + type: "youtube", + }; +}; + +export const mapYoutubeListResult = ( + video: youtube_v3.Schema$Video, +): PlaylistVideo => { + const videoId = video.id; + return { + url: "https://www.youtube.com/watch?v=" + videoId, + name: video.snippet?.title ?? "", + img: video.snippet?.thumbnails?.default?.url ?? "", + channel: video.snippet?.channelTitle ?? "", + duration: getVideoDuration(video.contentDetails?.duration ?? ""), + type: "youtube", + }; +}; + +export const mapYoutubePlaylistResult = ( + item: youtube_v3.Schema$PlaylistItem, +): PlaylistVideo => { + return { + url: "https://www.youtube.com/watch?v=" + item.snippet?.resourceId?.videoId, + name: item.snippet?.title ?? "", + img: item.snippet?.thumbnails?.default?.url ?? "", + channel: item.snippet?.channelTitle ?? "", + duration: 0, + // duration: getVideoDuration(video.contentDetails?.duration ?? ''), + type: "youtube", + }; +}; + +export const searchYoutube = async ( + query: string, +): Promise => { + const response = await Youtube?.search.list({ + part: ["snippet"], + type: ["video"], + maxResults: 25, + q: query, + }); + return response?.data?.items?.map(mapYoutubeSearchResult) ?? []; +}; + +export const youtubePlaylist = async ( + playlistId: string, +): Promise => { + const response = await Youtube?.playlistItems.list({ + part: ["snippet"], + playlistId, + maxResults: 100, + }); + return response?.data?.items?.map(mapYoutubePlaylistResult) ?? []; +}; + +export const getYoutubeVideoID = (url: string) => { + const idParts = YOUTUBE_VIDEO_ID_REGEX.exec(url); + if (!idParts) { + return; + } + + const id = idParts[1]; + if (!id) { + return; + } + + return id; +}; + +export const fetchYoutubeVideo = async ( + id: string, +): Promise => { + const response = await Youtube?.videos.list({ + part: ["snippet", "contentDetails"], + id: [id], + }); + const top = response?.data?.items?.[0]; + return top ? mapYoutubeListResult(top) : null; +}; + +export const getVideoDuration = (string: string): number => { + if (!string) { + return 0; + } + const hoursParts = PT_HOURS_REGEX.exec(string); + const minutesParts = PT_MINUTES_REGEX.exec(string); + const secondsParts = PT_SECONDS_REGEX.exec(string); + + const hours = hoursParts ? parseInt(hoursParts[1]) : 0; + const minutes = minutesParts ? parseInt(minutesParts[1]) : 0; + const seconds = secondsParts ? parseInt(secondsParts[1]) : 0; + + const totalSeconds = seconds + minutes * 60 + hours * 60 * 60; + return totalSeconds; +}; + +export const isYouTube = (input: string) => { + return ( + input.startsWith("https://www.youtube.com/") || + input.startsWith("https://youtu.be/") + ); +}; diff --git a/server/vm/base.ts b/server/vm/base.ts new file mode 100644 index 0000000..0a2f6eb --- /dev/null +++ b/server/vm/base.ts @@ -0,0 +1,625 @@ +import config from "../config.ts"; +import axios from "axios"; +import { redis, redisCount } from "../utils/redis.ts"; +import { postgres as pg } from "../utils/postgres.ts"; +import type { PoolConfig, PoolRegion } from "./utils.ts"; +import type { Client } from "pg"; + +const incrInterval = 5 * 1000; +const decrInterval = 15 * 1000; +const cleanupInterval = 5 * 60 * 1000; + +// If postgres isn't configured we can still run in stateless mode +// Only start/get/terminate can be used, otherwise exception will be thrown +const postgres = !pg ? (null as unknown as Client) : pg; + +export abstract class VMManager { + protected isLarge = false; + protected region: PoolRegion = "US"; + private limitSize = 0; + private minSize = 0; + protected hostname: string | undefined; + + constructor({ isLarge, region, limitSize, minSize, hostname }: PoolConfig) { + this.isLarge = isLarge; + this.region = region; + this.limitSize = Number(limitSize) || 0; + this.minSize = Number(minSize) || 0; + this.hostname = hostname; + } + + public getIsLarge = () => { + return this.isLarge; + }; + + public getRegion = () => { + return this.region; + }; + + public getMinSize = () => { + return this.minSize; + }; + + public getLimitSize = () => { + return this.limitSize; + }; + + public getTargetBuffer = () => { + let buffer = this.limitSize * 0.02; + // If ramping config, adjust buffer based on the hour + // During ramp down hours, keep a smaller buffer + // During ramp up hours, keep a larger buffer + // const rampDownHours = config.VM_POOL_RAMP_DOWN_HOURS.split(",").map(Number); + // const rampUpHours = config.VM_POOL_RAMP_UP_HOURS.split(",").map(Number); + // const nowHour = new Date().getUTCHours(); + // const isRampDown = + // rampDownHours.length && + // pointInInterval24(nowHour, rampDownHours[0], rampDownHours[1]); + // const isRampUp = + // rampUpHours.length && + // pointInInterval24(nowHour, rampUpHours[0], rampUpHours[1]); + // if (isRampDown) { + // buffer *= 0.5; + // } else if (isRampUp) { + // buffer *= 1.5; + // } + return Math.ceil(buffer); + }; + + public getCurrentSize = async () => { + const { rows } = await postgres.query( + `SELECT count(1) FROM vbrowser WHERE pool = $1`, + [this.getPoolName()], + ); + return Number(rows[0]?.count); + }; + + public getPoolName = () => { + return this.id + (this.isLarge ? "Large" : "") + this.region; + }; + + public getAvailableCount = async (): Promise => { + const { rows } = await postgres.query( + `SELECT count(1) FROM vbrowser WHERE pool = $1 and state = 'available'`, + [this.getPoolName()], + ); + return Number(rows[0]?.count); + }; + + public getStagingCount = async (): Promise => { + const { rows } = await postgres.query( + `SELECT count(1) FROM vbrowser WHERE pool = $1 and state = 'staging'`, + [this.getPoolName()], + ); + return Number(rows[0]?.count); + }; + + public getAvailableVBrowsers = async (): Promise => { + const { rows } = await postgres.query( + `SELECT vmid from vbrowser WHERE pool = $1 and state = 'available'`, + [this.getPoolName()], + ); + return rows.map((row: any) => row.vmid); + }; + + public getStagingVBrowsers = async (): Promise => { + const { rows } = await postgres.query( + `SELECT vmid from vbrowser WHERE pool = $1 and state = 'staging'`, + [this.getPoolName()], + ); + return rows.map((row: any) => row.vmid); + }; + + public getTag = () => { + return ( + (config.VBROWSER_TAG || "vbrowser") + + this.region + + (this.isLarge ? "Large" : "") + ); + }; + + public assignVM = async ( + roomId: string, + uid: string, + ): Promise => { + if (!roomId || !uid) { + return undefined; + } + // Update and use SKIP LOCKED to ensure each consumer gets a different one + const { rows } = await postgres.query( + ` + UPDATE vbrowser + SET "roomId" = $1, uid = $2, "heartbeatTime" = NOW(), "assignTime" = NOW(), state = 'used' + WHERE id = ( + SELECT id + FROM vbrowser + WHERE state = 'available' + AND pool = $3 + ORDER BY "creationTime" DESC + FOR UPDATE SKIP LOCKED + LIMIT 1 + ) + RETURNING data, pass`, + [roomId, uid, this.getPoolName()], + ); + let vm: VM | undefined = rows[0]?.data; + let pass: string = rows[0]?.pass; + if (!vm) { + return; + } + return { ...vm, pass, assignTime: Date.now() }; + }; + + public resetVM = async (vmid: string, roomId?: string): Promise => { + if (roomId !== undefined) { + // verify the roomId matches if user initiated + const { rows } = await postgres.query( + `SELECT "roomId" FROM vbrowser WHERE pool = $1 AND vmid = $2`, + [this.getPoolName(), vmid], + ); + if (rows[0]?.roomId && rows[0]?.roomId !== roomId) { + console.log( + "[RESET] %s: roomId mismatch on %s, expected %s, got %s", + this.getPoolName(), + vmid, + rows[0]?.roomId, + roomId, + ); + return; + } + } + console.log("[RESET]", this.getPoolName(), vmid, roomId); + // We generally want to reuse if the provider has per-hour billing + // Since most user sessions are less than an hour + // Otherwise if it's per-second or Docker, it's easier to just terminate it on reboot + if (this.reuseVMs) { + // To reset without API calls/reimaging (logic can be reused on any cloud provider so we can avoid individual reboot implementations): + // Update vbrowser script to generate uuid password + // add admin endpoints to vbrowser to get password and reboot + // to reset, hit reboot endpoint with admin key OR call cloud reboot method + // in checkstaging, check password endpoint with admin key, update DB once available + const { rows } = await postgres.query( + `SELECT image FROM vbrowser WHERE pool = $1 AND vmid = $2`, + [this.getPoolName(), vmid], + ); + const vmImageId = rows[0]?.image; + // Check if VM needs to be reimaged + if (this.imageId !== vmImageId) { + await this.reimageVM(vmid); + redisCount("vBrowserReimage"); + // Update the vmImageId + await postgres.query( + `UPDATE vbrowser SET image = $3 WHERE pool = $1 AND vmid = $2`, + [this.getPoolName(), vmid, this.imageId], + ); + } else { + await this.rebootVM(vmid); + } + // we could crash here and then row will remain in used state + // Once the heartbeat becomes stale cleanup will reset it again + const result = await postgres.query( + ` + INSERT INTO vbrowser(pool, vmid, "creationTime", state) + VALUES($1, $2, NOW(), 'staging') + ON CONFLICT(pool, vmid) DO + UPDATE SET state = 'staging', + "roomId" = NULL, uid = NULL, retries = 0, "heartbeatTime" = NULL, "assignTime" = NULL, data = NULL + `, + [this.getPoolName(), vmid], + ); + console.log("UPSERT", result.rowCount); + // Normally this should be an update, but we could insert if: + // if cleaning up a VM we didn't record in db on create + // if we resized down and deleted db row but didn't complete the termination + } else { + this.terminateVMWrapper(vmid); + } + }; + + public startVMWrapper = async () => { + // generate credentials and boot a VM + const password = crypto.randomUUID(); + const id = await this.startVM(password); + // We might fail to record it if crashing here but cleanup will reset it + await postgres.query( + ` + INSERT INTO vbrowser(pool, vmid, "creationTime", state, image) + VALUES($1, $2, NOW(), 'staging', $3)`, + [this.getPoolName(), id, this.imageId], + ); + redisCount("vBrowserLaunches"); + return id; + }; + + protected terminateVMWrapper = async (vmid: string) => { + console.log("[TERMINATE]", this.getPoolName(), vmid); + // Update the DB before calling terminate + // If we don't actually complete the termination, cleanup will reset it + const { command, rowCount } = await postgres.query( + `DELETE FROM vbrowser WHERE pool = $1 AND vmid = $2 RETURNING id`, + [this.getPoolName(), vmid], + ); + console.log(command, rowCount); + // We can log the VM lifetime by returning the creationTime and diffing + await this.terminateVM(vmid); + }; + + checkVMReady = async (host: string) => { + // NOTE: This URL doesn't work for docker since it doesn't have / + // But since we don't reuse docker VMs it's ok + const url = "https://" + host.replace("/", "/health"); + try { + // const out = execSync(`curl -i -L -v --ipv4 '${host}'`); + // if (!out.toString().startsWith('OK') && !out.toString().startsWith('404 page not found')) { + // throw new Error('mismatched response from health'); + // } + const resp = await axios({ + method: "GET", + url, + timeout: 1000, + }); + // Check to make sure the VM was recently rebooted (we could also check on the password to ensure reset) + const timeSinceBoot = Date.now() / 1000 - Number(resp.data); + // console.log(timeSinceBoot); + return this.reuseVMs ? timeSinceBoot < 150 * 1000 : true; + } catch (e) { + // console.log(url, e.message, e.response?.status); + return false; + } + }; + + public runBackgroundJobs = async () => { + const resizeVMGroupIncr = async () => { + const availableCount = await this.getAvailableCount(); + const stagingCount = await this.getStagingCount(); + const currentSize = await this.getCurrentSize(); + let launch = false; + launch = + availableCount + stagingCount < this.getTargetBuffer() && + currentSize < (this.getLimitSize() || Infinity); + if (launch) { + console.log( + "[RESIZE-INCR]", + this.getPoolName(), + "target:", + this.getTargetBuffer(), + "available:", + availableCount, + "staging:", + stagingCount, + "currentSize:", + currentSize, + "limit:", + this.getLimitSize(), + ); + try { + await this.startVMWrapper(); + } catch (e: any) { + console.log( + e.response?.status, + JSON.stringify(e.response?.data), + e.config?.url, + e.config?.data, + ); + } + } + }; + + const resizeVMGroupDecr = async () => { + const availableCount = await this.getAvailableCount(); + const stagingCount = await this.getStagingCount(); + let unlaunch = false; + unlaunch = availableCount + stagingCount > this.getTargetBuffer(); + if (unlaunch) { + // use SKIP LOCKED to delete to avoid deleting VM that might be assigning + // filter to only VMs eligible for deletion + // they must be up for long enough + // keep the oldest min pool size number of VMs + // Hetzner/DO/Scaleway rounds up to nearest hour + let modulo = 3600; + const { rows } = await postgres.query( + ` + DELETE FROM vbrowser + WHERE id = ( + SELECT id + FROM vbrowser + WHERE pool = $1 + AND state = 'available' + AND id >= (SELECT id from vbrowser WHERE pool = $1 ORDER BY id ASC LIMIT 1 OFFSET $2) + AND CAST(extract(epoch from now() - "creationTime") as INT) % $3 > $4 + FOR UPDATE SKIP LOCKED + LIMIT 1 + ) RETURNING vmid`, + [ + this.getPoolName(), + this.getMinSize(), + modulo, + config.VM_MIN_UPTIME_MINUTES * 60, // to seconds + ], + ); + const first = rows[0]; + if (first) { + console.log("[RESIZE-DECR] %s: %s", this.getPoolName(), first.vmid); + await this.terminateVMWrapper(first.vmid); + } + } + }; + + const cleanupVMGroup = async () => { + // Reset hanging VMs + // It's possible we created a VM but lost track of it + // Take the list of VMs from API + // subtract VMs that have a heartbeat or available or staging + let allVMs = []; + try { + allVMs = await this.listVMs(this.getTag()); + } catch (e) { + console.log( + "[CLEANUP] %s: failed to fetch VM list", + this.getPoolName(), + ); + return; + } + const { rows } = await postgres.query( + ` + SELECT vmid from vbrowser + WHERE pool = $1 + AND + ("heartbeatTime" > (NOW() - INTERVAL '5 minutes') + OR state = 'staging' + OR state = 'available') + `, + [this.getPoolName()], + ); + const inUse = new Set(rows.map((row: any) => row.vmid)); + console.log( + "[CLEANUP] %s: found %s VMs, %s to keep", + this.getPoolName(), + allVMs.length, + inUse.size, + ); + for (let server of allVMs) { + if (!inUse.has(server.id)) { + redisCount("vBrowserCleanup"); + console.log("[CLEANUP]", this.getPoolName(), server.id); + try { + await this.resetVM(server.id); + //this.terminateVMWrapper(server.id); + } catch (e: any) { + console.warn("[CLEANUP]", this.getPoolName(), e.response?.data); + } + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + } + }; + + const checkStaging = async () => { + // Increment retry count and return data + const { rows } = await postgres.query( + ` + UPDATE vbrowser + SET retries = retries + 1 + WHERE pool = $1 and state = 'staging' + RETURNING id, vmid, data, retries + `, + [this.getPoolName()], + ); + const stagingPromises = rows.map(async (row: any): Promise => { + const rowid = row.id; + const vmid: string = row.vmid; + const retryCount: number = row.retries; + let vm: VM | null = row.data; + if (retryCount < this.minRetries) { + if (config.NODE_ENV === "development") { + console.log( + "[CHECKSTAGING] %s: [vmid: %s] [attempt: %s] waiting for minRetries", + this.getPoolName(), + vmid, + retryCount, + ); + } + // Do a minimum # of retries to give reboot time + return [vmid, retryCount, false].join(","); + } + // Fetch data on first attempt + // Try again only every once in a while to reduce load on API + const shouldFetchVM = + retryCount === this.minRetries + 1 || retryCount % 30 === 0; + // Refetch the VM every once in a while even if cached to check if it should be deleted + if (shouldFetchVM) { + try { + vm = await this.getVM(vmid); + } catch (e: any) { + console.warn(e.response?.data); + if (e.response?.status === 404) { + // Remove the VM because the provider says it doesn't exist + await postgres.query("DELETE FROM vbrowser WHERE id = $1", [ + rowid, + ]); + throw new Error("failed to find vm " + vmid); + } + } + if (vm?.host) { + // Save the VM data + await postgres.query( + `UPDATE vbrowser SET data = $1 WHERE id = $2`, + [vm, rowid], + ); + } + } + if (!vm?.host) { + console.log( + "[CHECKSTAGING] %s: no host for vm %s", + this.getPoolName(), + vmid, + ); + throw new Error("no host for vm " + vmid); + } + if (retryCount % 150 === 0) { + console.log( + "[CHECKSTAGING] %s: %s poweron, attach to network", + this.getPoolName(), + vmid, + ); + this.powerOn(vmid); + //this.attachToNetwork(vmid); + } + if (retryCount % 180 === 0) { + console.log("[CHECKSTAGING]", this.getPoolName(), "giving up:", vmid); + redisCount("vBrowserStagingFails"); + await redis?.lpush("vBrowserStageFails", vmid); + await redis?.ltrim("vBrowserStageFails", 0, 19); + // VM didn't come up. set image to null so we reimage + await postgres.query( + `UPDATE vbrowser SET image = NULL WHERE pool = $1 AND vmid = $2`, + [this.getPoolName(), vmid], + ); + await this.resetVM(vmid); + } + if (retryCount >= 180) { + throw new Error("too many attempts on vm " + vmid); + } + const ready = await this.checkVMReady(vm.host); + if ( + ready || + retryCount % (config.NODE_ENV === "development" ? 1 : 30) === 0 + ) { + console.log( + "[CHECKSTAGING] %s: [ready: %s] [vmid: %s] [retries: %s] [host: %s]", + this.getPoolName(), + ready, + vmid, + retryCount, + vm?.host, + ); + } + if (ready) { + const passUrl = + "https://" + + (vm.host.includes("/") + ? vm.host.replace("/", "/password") + : vm.host + "/password") + + (vm.host.includes("?") ? "&" : "?") + + "key=" + + config.VBROWSER_ADMIN_KEY; + // console.log(passUrl); + const resp2 = await axios({ + method: "GET", + url: passUrl, + timeout: 1000, + }); + const pass = resp2.data; + // password is a uuid so it should never match the previous value, if it does we probably didn't reset properly + const { rows } = await postgres.query( + `UPDATE vbrowser SET state = 'available', pass = $2 WHERE id = $1 AND pass IS DISTINCT FROM $2`, + [rowid, pass], + ); + // console.log(rows); + await redis?.lpush("vBrowserStageRetries", retryCount); + await redis?.ltrim("vBrowserStageRetries", 0, 19); + } + return [vmid, retryCount, ready].join(","); + }); + // TODO log something if we timeout + const result = await Promise.race([ + Promise.allSettled(stagingPromises), + new Promise((resolve) => setTimeout(resolve, 30000)), + ]); + return result; + }; + + console.log("[VMWORKER] %s: starting background jobs", this.getPoolName()); + + setInterval(resizeVMGroupIncr, incrInterval); + setInterval(resizeVMGroupDecr, decrInterval); + setInterval(async () => { + console.log( + "[STATS] %s: currentSize %s, available %s, staging %s, target %s", + this.getPoolName(), + await this.getCurrentSize(), + await this.getAvailableCount(), + await this.getStagingCount(), + this.getTargetBuffer(), + ); + }, 10000); + + // The following may take a while per iteration + // Use while loop and delay between iterations rather than setInterval to avoid stacking requests + setImmediate(async () => { + while (true) { + console.time(this.getPoolName() + ":cleanup"); + try { + await cleanupVMGroup(); + } catch (e: any) { + console.warn( + "[CLEANUPVMGROUP-ERROR]", + this.getPoolName(), + e.response?.data, + ); + } + console.timeEnd(this.getPoolName() + ":cleanup"); + await new Promise((resolve) => setTimeout(resolve, cleanupInterval)); + } + }); + + setImmediate(async () => { + while (true) { + // console.time(this.getPoolName() + ':checkstaging'); + try { + await checkStaging(); + } catch (e) { + console.warn("[CHECKSTAGING-ERROR]", this.getPoolName(), e); + } + // console.timeEnd(this.getPoolName() + ':checkstaging'); + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + }); + }; + + public abstract id: string; + protected abstract size: string; + protected abstract largeSize: string; + protected abstract minRetries: number; + protected abstract reuseVMs: boolean; + protected abstract imageId: string; + protected abstract startVM: (name: string) => Promise; + protected abstract rebootVM: (id: string) => Promise; + protected abstract reimageVM: (id: string) => Promise; + protected abstract terminateVM: (id: string) => Promise; + public abstract getVM: (id: string) => Promise; + protected abstract listVMs: (filter: string) => Promise; + protected abstract powerOn: (id: string) => Promise; + protected abstract attachToNetwork: (id: string) => Promise; + protected abstract mapServerObject: (server: any) => VM; + public abstract updateSnapshot: () => Promise; +} + +function pointInInterval24(x: number, a: number, b: number) { + return nonNegativeMod(x - a, 24) <= nonNegativeMod(b - a, 24); +} + +function nonNegativeMod(n: number, m: number) { + return ((n % m) + m) % m; +} + +export interface VM { + id: string; + host: string; + provider: string; + large: boolean; + region: string; +} + +export interface AssignedVM extends VM { + pass: string; + assignTime: number; + controllerClient?: string; + creatorUID?: string; + creatorClientID?: string; +} + +export interface VMManagers { + standard: VMManager | null; + large: VMManager | null; + US: VMManager | null; +} diff --git a/server/vm/digitalocean.ts b/server/vm/digitalocean.ts new file mode 100644 index 0000000..2123653 --- /dev/null +++ b/server/vm/digitalocean.ts @@ -0,0 +1,139 @@ +import config from "../config.ts"; +import axios from "axios"; +import { VMManager, type VM } from "./base.ts"; + +const DO_TOKEN = config.DO_TOKEN; +const region = "sfo3"; +const gatewayHost = config.DO_GATEWAY; +const sshKeys = config.DO_SSH_KEYS.split(","); + +export class DigitalOcean extends VMManager { + size = "s-2vcpu-2gb"; // s-1vcpu-1gb, s-1vcpu-2gb, s-2vcpu-2gb, s-2vcpu-4gb, c-2, s-4vcpu-8gb + largeSize = "s-4vcpu-8gb"; + minRetries = 5; + reuseVMs = true; + id = "DO"; + imageId = config.DO_IMAGE; + startVM = async (name: string) => { + const response = await axios({ + method: "POST", + url: `https://api.digitalocean.com/v2/droplets`, + headers: { + Authorization: "Bearer " + DO_TOKEN, + "Content-Type": "application/json", + }, + data: { + name: name, + region: region, + size: this.isLarge ? this.largeSize : this.size, + image: Number(this.imageId), + ssh_keys: sshKeys, + private_networking: true, + // user_data: cloudInit(), + tags: [this.getTag()], + }, + }); + const id = response.data.droplet.id; + return id; + }; + + terminateVM = async (id: string) => { + const response = await axios({ + method: "DELETE", + url: `https://api.digitalocean.com/v2/droplets/${id}`, + headers: { + Authorization: "Bearer " + DO_TOKEN, + "Content-Type": "application/json", + }, + }); + }; + + rebootVM = async (id: string) => { + // Reboot the VM + const response2 = await axios({ + method: "POST", + url: `https://api.digitalocean.com/v2/droplets/${id}/actions`, + headers: { + Authorization: "Bearer " + DO_TOKEN, + "Content-Type": "application/json", + }, + data: { + type: "reboot", + }, + }); + return; + }; + + reimageVM = async (id: string) => { + // Rebuild the VM + const response2 = await axios({ + method: "POST", + url: `https://api.digitalocean.com/v2/droplets/${id}/actions`, + headers: { + Authorization: "Bearer " + DO_TOKEN, + "Content-Type": "application/json", + }, + data: { + type: "rebuild", + image: Number(this.imageId), + }, + }); + return; + }; + + getVM = async (id: string) => { + const response = await axios({ + method: "GET", + url: `https://api.digitalocean.com/v2/droplets/${id}`, + headers: { + Authorization: "Bearer " + DO_TOKEN, + "Content-Type": "application/json", + }, + }); + let server = this.mapServerObject(response.data.droplet); + return server; + }; + + listVMs = async (filter: string) => { + // console.log(filter, tags); + const response = await axios({ + method: "GET", + url: `https://api.digitalocean.com/v2/droplets`, + headers: { + Authorization: "Bearer " + DO_TOKEN, + "Content-Type": "application/json", + }, + params: { + // TODO need to update if over 100 results + per_page: 100, + tag_name: filter, + }, + }); + return response.data.droplets.map(this.mapServerObject); + }; + + powerOn = async (_id: string) => {}; + + attachToNetwork = async (_id: string) => {}; + + updateSnapshot = async () => { + return ""; + }; + + mapServerObject = (server: any): VM => { + // const ip = server.networks.v4.find( + // (network: any) => network.type === 'private', + // )?.ip_address; + const ip = server.networks.v4.find( + (network: any) => network.type === "public", + )?.ip_address; + return { + id: server.id?.toString(), + // The gateway handles SSL termination and proxies to the private IP + host: ip ? `${gatewayHost}/?ip=${ip}` : "", + provider: this.id, + large: this.isLarge, + region: this.region, + }; + }; +} diff --git a/server/vm/docker.ts b/server/vm/docker.ts new file mode 100644 index 0000000..ad6bf18 --- /dev/null +++ b/server/vm/docker.ts @@ -0,0 +1,134 @@ +// This assumes an installation of Docker exists on the given host +// and that host is configured to accept our SSH key +import config from "../config.ts"; +import { VMManager, type VM } from "./base.ts"; +import { imageName } from "./utils.ts"; +import fs from "node:fs"; +import { homedir } from "node:os"; +import { NodeSSH } from "node-ssh"; + +export class Docker extends VMManager { + // TODO support multiple Docker providers in the pool config with same region + size = ""; + largeSize = ""; + minRetries = 0; + reuseVMs = false; + id = "Docker"; + ssh: NodeSSH | undefined = undefined; + imageId = imageName; + + getSSH = async () => { + if (this.ssh && this.ssh.isConnected()) { + return this.ssh; + } + const sshConfig = { + username: config.DOCKER_VM_HOST_SSH_USER, + host: this.hostname, + // The private key the Docker host is configured to accept + privateKey: config.DOCKER_VM_HOST_SSH_KEY_BASE64 + ? Buffer.from(config.DOCKER_VM_HOST_SSH_KEY_BASE64, "base64").toString() + : fs.readFileSync(homedir() + "/.ssh/id_rsa").toString(), + }; + this.ssh = new NodeSSH(); + await this.ssh.connect(sshConfig); + return this.ssh; + }; + + startVM = async (name: string) => { + const tag = this.getTag(); + const conn = await this.getSSH(); + // If in development, have neko share the same SSL cert as the other services + // If in production, they are probably on different hosts and neko is behind a reverse proxy for SSL termination + const sslEnv = + config.NODE_ENV === "development" && + config.SSL_KEY_FILE && + config.SSL_CRT_FILE + ? `-e NEKO_KEY="${config.SSL_KEY_FILE}" -e NEKO_CERT="${config.SSL_CRT_FILE}"` + : ""; + const { stdout, stderr } = await conn.execCommand( + ` + #!/bin/bash + set -e + PORT=$(comm -23 <(seq 5000 5063 | sort) <(ss -Htan | awk '{print $4}' | cut -d':' -f2 | sort -u) | sort -n | head -n 1) + INDEX=$(($PORT - 5000)) + UDP_START=$((59000+$INDEX*100)) + UDP_END=$((59099+$INDEX*100)) + docker run -d --rm --name=${name} --memory="2g" --cpus="2" -p $PORT:$PORT -p $UDP_START-$UDP_END:$UDP_START-$UDP_END/udp -v /etc/letsencrypt:/etc/letsencrypt -l ${tag} -l index=$INDEX --log-opt max-size=1g --shm-size=1g --cap-add="SYS_ADMIN" ${sslEnv} -e DISPLAY=":99.0" -e NEKO_PASSWORD=${name} -e NEKO_PASSWORD_ADMIN=${name} -e NEKO_ADMIN_KEY=${config.VBROWSER_ADMIN_KEY} -e NEKO_BIND=":$PORT" -e NEKO_EPR=":$UDP_START-$UDP_END" -e NEKO_H264="1" ${imageName} + `, + ); + console.log(stdout, stderr); + return stdout.trim(); + }; + + terminateVM = async (id: string) => { + const conn = await this.getSSH(); + const { stdout, stderr } = await conn.execCommand(`docker rm -fv ${id}`); + console.log(stdout, stderr); + return; + }; + + rebootVM = async (id: string) => { + // Docker containers aren't set to reuse, so do nothing (reset will terminate) + }; + + reimageVM = async (id: string) => { + const conn = await this.getSSH(); + const { stdout, stderr } = await conn.execCommand( + `docker pull ${this.imageId}`, + ); + console.log(stdout, stderr); + // The container is out of date. Delete it + this.terminateVMWrapper(id); + return; + }; + + getVM = async (id: string) => { + const conn = await this.getSSH(); + const { stdout } = await conn.execCommand(`docker inspect ${id}`); + let data = null; + try { + data = JSON.parse(stdout)[0]; + if (!data) { + throw new Error("no container with this ID found"); + } + } catch { + console.warn(stdout); + throw new Error("failed to parse json"); + } + let server = this.mapServerObject(data); + return server; + }; + + listVMs = async (filter: string) => { + const conn = await this.getSSH(); + const listCmd = `docker inspect $(docker ps --filter label=${filter} --quiet --no-trunc)`; + const { stdout } = await conn.execCommand(listCmd); + if (!stdout) { + return []; + } + let data = []; + try { + data = JSON.parse(stdout); + } catch (e) { + console.warn(stdout); + throw new Error("failed to parse json"); + } + return data.map(this.mapServerObject); + }; + + powerOn = async (id: string) => {}; + + attachToNetwork = async (id: string) => {}; + + updateSnapshot = async () => { + return ""; + }; + + mapServerObject = (server: any): VM => ({ + id: server.Id, + host: `${this.hostname}:${5000 + Number(server.Config?.Labels?.index)}`, + provider: this.id, + large: this.isLarge, + region: this.region, + }); +} diff --git a/server/vm/hetzner.ts b/server/vm/hetzner.ts new file mode 100644 index 0000000..3c4adab --- /dev/null +++ b/server/vm/hetzner.ts @@ -0,0 +1,258 @@ +import config from "../config.ts"; +import axios from "axios"; +import { VMManager, type VM } from "./base.ts"; +import fs from "node:fs"; +import { redis } from "../utils/redis.ts"; + +const HETZNER_TOKEN = config.HETZNER_TOKEN; +const sshKeys = config.HETZNER_SSH_KEYS.split(",").map(Number); + +export class Hetzner extends VMManager { + size = "cpx11"; // cpx11, cx22 (not available in US yet) + largeSize = "cpx31"; // cpx21, cpx31, ccx13, ccx23, cx32 + minRetries = 5; + reuseVMs = true; + id = "Hetzner"; + gateway = config.HETZNER_GATEWAY; + imageId = config.HETZNER_IMAGE; + + private getRandomDatacenter() { + // US + let datacenters = ["ash"]; + if (this.region === "USW") { + datacenters = ["hil"]; + } else if (this.region === "EU") { + datacenters = ["nbg1", "fsn1", "hel1"]; + } + return datacenters[Math.floor(Math.random() * datacenters.length)]; + } + + startVM = async (name: string) => { + const data = { + name: name, + server_type: this.isLarge ? this.largeSize : this.size, + start_after_create: true, + image: Number(this.imageId), + ssh_keys: sshKeys, + public_net: { + enable_ipv4: true, + enable_ipv6: false, + }, + // networks: [ + // this.networks[Math.floor(Math.random() * this.networks.length)], + // ], + // user_data: `replace with vbrowser.sh startup script if we want to boot vbrowser on instance creation (won't trigger on rebuild/restart)` + labels: { + [this.getTag()]: "1", + }, + location: this.getRandomDatacenter(), + }; + const response = await axios({ + method: "POST", + url: `https://api.hetzner.cloud/v1/servers`, + headers: { + Authorization: "Bearer " + HETZNER_TOKEN, + "Content-Type": "application/json", + "User-Agent": "test", + }, + data, + }); + const id = response.data.server.id; + return id; + }; + + terminateVM = async (id: string) => { + await axios({ + method: "DELETE", + url: `https://api.hetzner.cloud/v1/servers/${id}`, + headers: { + Authorization: "Bearer " + HETZNER_TOKEN, + "User-Agent": "test", + }, + }); + }; + + rebootVM = async (id: string) => { + // Reboot the VM + await axios({ + method: "POST", + url: `https://api.hetzner.cloud/v1/servers/${id}/actions/reboot`, + headers: { + Authorization: "Bearer " + HETZNER_TOKEN, + "User-Agent": "test", + }, + }); + return; + }; + + reimageVM = async (id: string) => { + // Rebuild the VM + await axios({ + method: "POST", + url: `https://api.hetzner.cloud/v1/servers/${id}/actions/rebuild`, + headers: { + Authorization: "Bearer " + HETZNER_TOKEN, + "User-Agent": "test", + }, + data: { + image: Number(this.imageId), + }, + }); + return; + }; + + getVM = async (id: string) => { + const response: any = await axios({ + method: "GET", + url: `https://api.hetzner.cloud/v1/servers/${id}`, + headers: { + Authorization: "Bearer " + HETZNER_TOKEN, + "User-Agent": "test", + }, + }); + console.log( + "[GETVM] %s: %s rate limit remaining", + id, + response?.headers["ratelimit-remaining"], + ); + await redis?.set( + "hetznerApiRemaining", + response?.headers["ratelimit-remaining"], + ); + const server = this.mapServerObject(response.data.server); + return server; + }; + + listVMs = async (filter: string) => { + const limit = this.getLimitSize(); + const pageCount = Math.ceil((limit || 1) / 50); + const pages = Array.from(Array(pageCount).keys()).map((i) => i + 1); + const responses: any[] = await Promise.all( + pages.map((page) => + axios({ + method: "GET", + url: `https://api.hetzner.cloud/v1/servers`, + headers: { + Authorization: "Bearer " + HETZNER_TOKEN, + "User-Agent": "test", + }, + params: { + sort: "id:asc", + page, + per_page: 50, + label_selector: filter, + }, + }), + ), + ); + const responsesMapped = responses.map((response) => + response.data.servers.map(this.mapServerObject), + ); + return responsesMapped.flat(); + }; + + powerOn = async (id: string) => { + // Poweron the server (usually not needed) + try { + await axios({ + method: "POST", + url: `https://api.hetzner.cloud/v1/servers/${id}/actions/poweron`, + headers: { + Authorization: "Bearer " + HETZNER_TOKEN, + "Content-Type": "application/json", + "User-Agent": "test", + }, + }); + } catch (e) { + console.log("%s failed to poweron", id); + } + }; + + attachToNetwork = async (id: string) => { + // // Attach server to network (usually not needed) + // try { + // const response: any = await axios({ + // method: 'GET', + // url: `https://api.hetzner.cloud/v1/servers/${id}`, + // headers: { + // Authorization: 'Bearer ' + HETZNER_TOKEN, + // }, + // }); + // if (response.data.server.private_net?.[0] == null) { + // await axios({ + // method: 'POST', + // url: `https://api.hetzner.cloud/v1/servers/${id}/actions/attach_to_network`, + // headers: { + // Authorization: 'Bearer ' + HETZNER_TOKEN, + // 'Content-Type': 'application/json', + // }, + // data: { + // network: + // this.networks[Math.floor(Math.random() * this.networks.length)], + // }, + // }); + // } + // } catch (e: any) { + // console.log('%s failed to attach to network', id); + // console.log(e.response?.data); + // } + }; + + updateSnapshot = async () => { + const response = await axios({ + method: "POST", + url: `https://api.hetzner.cloud/v1/servers`, + headers: { + Authorization: "Bearer " + HETZNER_TOKEN, + "Content-Type": "application/json", + "User-Agent": "test", + }, + data: { + name: "vBrowserSnapshot", + server_type: "cpx11", + start_after_create: true, + image: "docker-ce", // 15512617 for Ubuntu 20.04 + ssh_keys: sshKeys, + user_data: fs + .readFileSync(import.meta.dirname + "/../../dev/vbrowser.sh") + .toString() + .replace("{VBROWSER_ADMIN_KEY}", config.VBROWSER_ADMIN_KEY), + location: this.getRandomDatacenter(), + }, + }); + const id = response.data.server.id; + await new Promise((resolve) => setTimeout(resolve, 4 * 60 * 1000)); + // Validate snapshot server was created successfully + // const response3 = await axios( + // 'http://' + response.data.server.public_net?.ipv4?.ip + ':5000' + // ); + const response2 = await axios({ + method: "POST", + url: `https://api.hetzner.cloud/v1/servers/${id}/actions/create_image`, + headers: { + Authorization: "Bearer " + HETZNER_TOKEN, + "Content-Type": "application/json", + }, + }); + const imageId = response2.data.image.id; + await this.terminateVM(id); + return imageId; + }; + + mapServerObject = (server: any): VM => { + const public_ip = server.public_net?.ipv4?.ip; + // const private_ip = server.private_net?.[0]?.ip; + const ip = public_ip; + // We can use either the public or private IP for communicating between gateway and VM + // Only signaling traffic goes through here since the video is transmitted over WebRTC + // The private IP requires the server and gateway to be on the same network and there is a limit to the number of servers allowed + return { + id: server.id?.toString(), + // The gateway handles SSL termination and proxies to the private IP + host: ip ? `${this.gateway}/?ip=${ip}` : "", + provider: this.id, + large: this.isLarge, + region: this.region, + }; + }; +} diff --git a/server/vm/scaleway.ts b/server/vm/scaleway.ts new file mode 100644 index 0000000..d531fd3 --- /dev/null +++ b/server/vm/scaleway.ts @@ -0,0 +1,159 @@ +import config from "../config.ts"; +import axios from "axios"; +import { VMManager, type VM } from "./base.ts"; + +const SCW_SECRET_KEY = config.SCW_SECRET_KEY; +const SCW_ORGANIZATION_ID = config.SCW_ORGANIZATION_ID; +const region = "nl-ams-1"; //fr-par-1 +const gatewayHost = config.SCW_GATEWAY; + +export class Scaleway extends VMManager { + size = "DEV1-S"; // DEV1-S, DEV1-M, DEV1-L, GP1-XS + largeSize = "DEV1-M"; + minRetries = 5; + reuseVMs = true; + id = "Scaleway"; + imageId = config.SCW_IMAGE; + startVM = async (name: string) => { + const response = await axios({ + method: "POST", + url: `https://api.scaleway.com/instance/v1/zones/${region}/servers`, + headers: { + "X-Auth-Token": SCW_SECRET_KEY, + "Content-Type": "application/json", + }, + data: { + name: name, + dynamic_ip_required: true, + commercial_type: this.isLarge ? this.largeSize : this.size, + image: this.imageId, + volumes: {}, + organization: SCW_ORGANIZATION_ID, + tags: [this.getTag()], + }, + }); + // console.log(response.data); + const id = response.data.server.id; + const response2 = await axios({ + method: "PATCH", + url: `https://api.scaleway.com/instance/v1/zones/${region}/servers/${id}/user_data/cloud-init`, + headers: { + "X-Auth-Token": SCW_SECRET_KEY, + "Content-Type": "text/plain", + }, + // set userdata for boot action + //data: cloudInit(), + }); + // console.log(response2.data); + // boot the instance + const response3 = await axios({ + method: "POST", + url: `https://api.scaleway.com/instance/v1/zones/${region}/servers/${id}/action`, + headers: { + "X-Auth-Token": SCW_SECRET_KEY, + "Content-Type": "application/json", + }, + data: { + action: "poweron", + }, + }); + // console.log(response3.data); + return id; + }; + + terminateVM = async (id: string) => { + const response = await axios({ + method: "POST", + url: `https://api.scaleway.com/instance/v1/zones/${region}/servers/${id}/action`, + headers: { + "X-Auth-Token": SCW_SECRET_KEY, + "Content-Type": "application/json", + }, + data: { + action: "terminate", + }, + }); + }; + + rebootVM = async (id: string) => { + // Reboot the VM (also destroys the Docker container since it has --rm flag) + const response2 = await axios({ + method: "POST", + url: `https://api.scaleway.com/instance/v1/zones/${region}/servers/${id}/action`, + headers: { + "X-Auth-Token": SCW_SECRET_KEY, + "Content-Type": "application/json", + }, + data: { + action: "reboot", + }, + }); + return; + }; + + reimageVM = async (id: string) => { + // Scaleway doesn't have a reimage/rebuild command. Delete the VM + await this.terminateVMWrapper(id); + }; + + getVM = async (id: string) => { + const response = await axios({ + method: "GET", + url: `https://api.scaleway.com/instance/v1/zones/${region}/servers/${id}`, + headers: { + "X-Auth-Token": SCW_SECRET_KEY, + "Content-Type": "application/json", + }, + }); + let server = this.mapServerObject(response.data.server); + return server; + }; + + listVMs = async (filter: string) => { + const limit = this.getLimitSize(); + const pageCount = Math.ceil((limit || 1) / 100); + const pages = Array.from(Array(pageCount).keys()).map((i) => i + 1); + const responses: any[] = await Promise.all( + pages.map((page) => + axios({ + method: "GET", + url: `https://api.scaleway.com/instance/v1/zones/${region}/servers`, + headers: { + "X-Auth-Token": SCW_SECRET_KEY, + "Content-Type": "application/json", + }, + params: { + page, + per_page: 100, + tags: filter, + }, + }), + ), + ); + const responsesMapped = responses.map((response) => + response.data.servers.map(this.mapServerObject), + ); + return responsesMapped.flat(); + }; + + powerOn = async (_id: string) => {}; + + attachToNetwork = async (_id: string) => {}; + + updateSnapshot = async () => { + return ""; + }; + + mapServerObject = (server: any): VM => { + // const ip = server.private_ip; + const ip = server.public_ip?.address; + return { + id: server.id, + // The gateway handles SSL termination and proxies to the private IP + host: ip ? `${gatewayHost}/?ip=${ip}` : "", + provider: this.id, + large: this.isLarge, + region: this.region, + }; + }; +} diff --git a/server/vm/utils.ts b/server/vm/utils.ts new file mode 100644 index 0000000..ec4ddc7 --- /dev/null +++ b/server/vm/utils.ts @@ -0,0 +1,86 @@ +import { type AssignedVM, VMManager } from "./base.ts"; +import config from "../config.ts"; +import { Scaleway } from "./scaleway.ts"; +import { Hetzner } from "./hetzner.ts"; +import { DigitalOcean } from "./digitalocean.ts"; +import { Docker } from "./docker.ts"; + +// Chromium on ARM: ghcr.io/howardchung/vbrowser/arm-chromium +export const imageName = "howardc93/vbrowser"; + +export type PoolRegion = "US" | "USW" | "EU"; +export type PoolConfig = { + provider: string; + isLarge: boolean; + region: PoolRegion; + limitSize: number | undefined; + minSize: number | undefined; + hostname: string | undefined; +}; + +function createVMManager(poolConfig: PoolConfig): VMManager { + let vmManager: VMManager | null = null; + if ( + config.SCW_SECRET_KEY && + config.SCW_ORGANIZATION_ID && + config.SCW_IMAGE && + config.SCW_GATEWAY && + poolConfig.provider === "Scaleway" + ) { + vmManager = new Scaleway(poolConfig); + } else if ( + config.HETZNER_TOKEN && + config.HETZNER_IMAGE && + config.HETZNER_GATEWAY && + poolConfig.provider === "Hetzner" + ) { + vmManager = new Hetzner(poolConfig); + } else if ( + config.DO_TOKEN && + config.DO_IMAGE && + config.DO_GATEWAY && + poolConfig.provider === "DO" + ) { + vmManager = new DigitalOcean(poolConfig); + } else if (poolConfig.provider === "Docker") { + vmManager = new Docker(poolConfig); + } + if (!vmManager) { + throw new Error("failed to create vmManager"); + } + return vmManager; +} + +export function getVMManagerConfig(): PoolConfig[] { + return config.VM_MANAGER_CONFIG.split(",") + .filter(Boolean) + .map((c) => { + const split = c.split(":"); + return { + provider: split[0], + isLarge: split[1] === "large", + region: split[2] as PoolRegion, + minSize: Number(split[3]), + limitSize: Number(split[4]), + hostname: split[5], + }; + }); +} + +export function getBgVMManagers(): { [key: string]: VMManager } { + const result: { [key: string]: VMManager } = {}; + const conf = getVMManagerConfig(); + conf.forEach((c) => { + const mgr = createVMManager(c); + if (mgr) { + result[mgr.getPoolName()] = mgr; + } + }); + return result; +} + +export function getSessionLimitSeconds(isLarge: boolean) { + return isLarge + ? config.VBROWSER_SESSION_SECONDS_LARGE + : config.VBROWSER_SESSION_SECONDS; +} diff --git a/server/vmWorker.ts b/server/vmWorker.ts new file mode 100644 index 0000000..d804c3e --- /dev/null +++ b/server/vmWorker.ts @@ -0,0 +1,114 @@ +import config from "./config.ts"; +import { getBgVMManagers } from "./vm/utils.ts"; +import express from "express"; +import bodyParser from "body-parser"; + +const app = express(); +const vmManagers = getBgVMManagers(); + +app.use(bodyParser.json()); + +Object.values(vmManagers).forEach((manager) => { + manager?.runBackgroundJobs(); +}); + +app.post("/assignVM", async (req, res) => { + try { + // Find a pool that matches the size and region requirements + const pools = Object.values(vmManagers).filter((mgr) => { + return ( + mgr.getIsLarge() === Boolean(req.body.isLarge) && + (mgr.getRegion() === req.body.region || !req.body.region) + ); + }); + let vm = null; + // Sequentially try each to give earlier pools preference + // We might want to add the ability to load balance as well by randomly selecting between pools with same priority + for (let pool of pools) { + console.log( + "try assignVM from pool:", + pool.getPoolName(), + req.body.roomId, + req.body.uid, + ); + vm = await pool.assignVM(req.body.roomId, req.body.uid); + if (vm) { + res.json(vm); + return; + } + } + res.json(null); + } catch (e) { + console.warn(e); + res.status(500).end(); + } +}); + +app.post("/releaseVM", async (req, res) => { + try { + const pool = + vmManagers[ + req.body.provider + (req.body.isLarge ? "Large" : "") + req.body.region + ]; + if (req.body.id) { + await pool?.resetVM(req.body.id, req.body.roomId); + } + res.end(); + } catch (e) { + console.warn(e); + res.status(500).end(); + } +}); + +app.get("/stats", async (req, res) => { + const vmManagerStats: AnyDict = {}; + for (let [key, vmManager] of Object.entries(vmManagers)) { + const availableVBrowsers = await vmManager?.getAvailableVBrowsers(); + const stagingVBrowsers = await vmManager?.getStagingVBrowsers(); + const size = await vmManager?.getCurrentSize(); + if (key && vmManager) { + vmManagerStats[key] = { + availableVBrowsers, + stagingVBrowsers, + bufferSize: vmManager?.getTargetBuffer(), + // terminationVBrowsers, + size, + }; + } + } + res.json(vmManagerStats); +}); + +app.get("/isFreePoolFull", async (req, res) => { + const freePools = Object.values(vmManagers).filter((mgr) => { + return mgr?.getIsLarge() === false && mgr?.getLimitSize() > 0; + }); + const fullResult = await Promise.all( + freePools.map(async (freePool) => { + let isFull = false; + if (freePool) { + const availableCount = await freePool.getAvailableCount(); + const limitSize = freePool?.getLimitSize() ?? 0; + const currentSize = await freePool.getCurrentSize(); + isFull = Boolean( + limitSize > 0 && + (Number(availableCount) === 0 || + Number(currentSize) - Number(availableCount) > limitSize * 0.95), + ); + } + return isFull; + }), + ); + const isFull = freePools.length && fullResult.every(Boolean); + res.json({ isFull }); +}); + +app.post("/updateSnapshot", async (req, res) => { + const pool = vmManagers[req.body.provider + req.body.region]; + const result = await pool?.updateSnapshot(); + res.send(result?.toString() + "\n"); +}); + +app.listen(config.VMWORKER_PORT, () => { + console.log("vmWorker listening on %s", config.VMWORKER_PORT); +}); diff --git a/sql/migrations.sql b/sql/migrations.sql new file mode 100644 index 0000000..d4bee8b --- /dev/null +++ b/sql/migrations.sql @@ -0,0 +1,3 @@ +alter table room add column "roomTitle" text; +alter table room add column "roomDescription" text; +alter table room add column "roomTitleColor" text; \ No newline at end of file diff --git a/sql/schema.sql b/sql/schema.sql new file mode 100644 index 0000000..cd5b6c6 --- /dev/null +++ b/sql/schema.sql @@ -0,0 +1,65 @@ +CREATE EXTENSION pg_trgm; + +CREATE TABLE room( + "roomId" text, + "creationTime" timestamp with time zone, + password text, + owner text, + vanity text, + "isChatDisabled" boolean, + "isSubRoom" boolean, + data jsonb, + "lastUpdateTime" timestamp with time zone, + "roomTitle" text, + "roomDescription" text, + "roomTitleColor" text, + "mediaPath" text, + PRIMARY KEY ("roomId") +); +CREATE UNIQUE INDEX on room(LOWER(vanity)) WHERE vanity IS NOT NULL; +CREATE INDEX on room(owner) WHERE owner IS NOT NULL; +CREATE INDEX on room("creationTime"); +CREATE INDEX on room USING GIN("roomId" gin_trgm_ops); + +CREATE TABLE subscriber( + "customerId" text, + email text, + status text, + uid text, + PRIMARY KEY(uid) +); + +CREATE TABLE link_account( + uid text, + kind text, + accountid text, + accountname text, + discriminator text, + PRIMARY KEY(uid, kind) +); + +CREATE TABLE active_user( + uid text, + "lastActiveTime" timestamp with time zone, + PRIMARY KEY(uid) +); + +CREATE TABLE vbrowser( + id bigserial PRIMARY KEY, -- numeric sequence ID + pool text NOT NULL, -- name of the pool this VM is in, e.g. HetznerLargeUS + vmid text NOT NULL, -- identifier for the VM, only unique across a provider + state text NOT NULL, -- available, staging, used + "creationTime" timestamp with time zone NOT NULL, -- time the VM was created + "heartbeatTime" timestamp with time zone, -- last time a room reported this VM was in use + "assignTime" timestamp with time zone, -- last time the room was assigned + "roomId" text, -- room VM assigned to + uid text, -- user requesting the VM + data json, -- metadata for the VM + retries int DEFAULT 0, -- how many times we checked if VM is up + pass text, -- password to access vbrowser + image text -- ID of the last image applied to this VM +); +CREATE UNIQUE INDEX on vbrowser(pool, vmid); +CREATE INDEX on vbrowser(pool, state); +CREATE INDEX on vbrowser("roomId"); +CREATE INDEX on vbrowser(uid); diff --git a/src/MetadataContext.ts b/src/MetadataContext.ts new file mode 100644 index 0000000..8b75c4a --- /dev/null +++ b/src/MetadataContext.ts @@ -0,0 +1,12 @@ +import firebase from "firebase/compat/app"; +import React from "react"; + +export const DEFAULT_STATE = { + user: undefined as firebase.User | undefined, + isSubscriber: undefined as boolean | undefined, + streamPath: undefined as string | undefined, + convertPath: undefined as string | undefined, + beta: false, +}; + +export const MetadataContext = React.createContext(DEFAULT_STATE); diff --git a/src/components/Announce/Announce.module.css b/src/components/Announce/Announce.module.css new file mode 100644 index 0000000..998bec8 --- /dev/null +++ b/src/components/Announce/Announce.module.css @@ -0,0 +1,38 @@ +.announce { + display: flex; + flex-direction: row; + align-items: center; + background-color: #008eff; + width: 500px; + max-width: 100%; + position: fixed; + top: 0px; + left: 50%; + transform: translate(-50%, 0); + border-radius: 4px; + z-index: 900; + height: 48px; + color: white; + overflow: hidden; +} + +@media only screen and (max-width: 600px) { + .announce { + bottom: 0px; + top: initial; + } +} + +.announce main { + padding: 6px 10px; + flex-grow: 1; +} + +.announce main a { + color: blue; + text-decoration: underline; +} + +.announce aside { + padding: 6px 10px; +} diff --git a/src/components/Announce/Announce.tsx b/src/components/Announce/Announce.tsx new file mode 100644 index 0000000..a450f6a --- /dev/null +++ b/src/components/Announce/Announce.tsx @@ -0,0 +1,61 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { Button } from "@mantine/core"; +import styles from "./Announce.module.css"; +import config from "../../config"; + +const GITHUB_REPO = "howardchung/watchparty-announcements"; + +type Issue = { + title: string; + body: string; + number: number; + updated_at: string; +}; +const Announce = () => { + const [announcement, setAnnouncement] = useState(null); + useEffect(() => { + const update = async () => { + const response = await fetch( + "https://api.github.com/search/issues?" + + new URLSearchParams({ + q: `repo:${GITHUB_REPO} label:${ + config.NODE_ENV === "development" ? "test" : "release" + }`, + order: "desc", + page: "1", + per_page: "1", + }), + ); + const data: { items: Issue[] } = await response.json(); + // console.log(data); + const top = data?.items?.[0]; + if ( + top?.number > Number(localStorage.getItem("announcement-dismiss")) && + new Date(top.updated_at) > + new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) + ) { + setAnnouncement(top); + } + }; + update(); + }, []); + + const onDismiss = useCallback((value: number) => { + localStorage.setItem("announcement-dismiss", value.toString()); + setAnnouncement(null); + }, []); + + return announcement != null ? ( +
+ {/*

{announcement.title}

*/} +
{announcement.body}
+ +
+ ) : null; +}; + +export default Announce; diff --git a/src/components/App/App.module.css b/src/components/App/App.module.css new file mode 100644 index 0000000..21c6b21 --- /dev/null +++ b/src/components/App/App.module.css @@ -0,0 +1,79 @@ +:global(#leftVideo)::-webkit-media-controls { + display: none !important; +} + +.videoContent { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.playerContainer { + height: 100%; + position: relative; + display: flex; + align-items: center; + justify-content: center; + /* Making this larger causes issues on mobile */ + min-height: 48vh; +} + +.flexCenter { + display: flex; + align-items: center; + justify-content: center; +} + +.mobileStack { + display: flex; + gap: 4px; + @media only screen and (max-width: 600px) { + flex-direction: column; + width: calc(100% - 8px); + } +} + +.mobileWrap { + @media only screen and (max-width: 600px) { + flex-wrap: wrap; + } +} + +.leftColumn { + flex-grow: 1; +} + +.rightColumn { + flex-shrink: 0; +} + +.expandButton { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + top: 50%; + right: -30px; +} + +.fullHeightColumn { + height: calc(100vh - 72px); + transition: width ease-out 0.5s; + &.leftColumn { + @media only screen and (max-width: 600px) { + /* video height doesn't matter on mobile since things are stacked */ + height: initial; + } + } +} + +.fullHeightColumnFullscreen { + height: calc(100vh); + transition: width ease-out 0.5s; +} + +.shareButton { + min-width: 150px; +} diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx new file mode 100644 index 0000000..c8dd7f3 --- /dev/null +++ b/src/components/App/App.tsx @@ -0,0 +1,2711 @@ +import type MediasoupClient from "mediasoup-client"; +import React from "react"; +import { Alert, Loader, Menu, Overlay, Select, Title } from "@mantine/core"; +import io, { Socket } from "socket.io-client"; +import { + formatSpeed, + iceServers, + isMobile, + serverPath, + testAutoplay, + openFileSelector, + getOrCreateClientId, + getOrCreateSessionId, + calculateMedian, + getUserImage, + isYouTube, + isMagnet, + isHttp, + isHls, + isScreenShare, + isFileShare, + isVBrowser, + isDash, + VIDEO_MAX_HEIGHT_CSS, + createUuid, + softWhite, + getSavedPasswords, +} from "../../utils/utils"; +import { generateName } from "../../utils/generateName"; +import { Chat } from "../Chat/Chat"; +import { TopBar } from "../TopBar/TopBar"; +import { VBrowser } from "../VBrowser/VBrowser"; +import { VideoChat } from "../VideoChat/VideoChat"; +import { getCurrentSettings } from "../Settings/LocalSettings"; +import { MultiStreamModal } from "../Modal/MultiStreamModal"; +import { ComboBox } from "../ComboBox/ComboBox"; +import { SearchComponent } from "../SearchComponent/SearchComponent"; +import { Controls } from "../Controls/Controls"; +import { VBrowserModal } from "../Modal/VBrowserModal"; +import { SettingsModal } from "../Settings/SettingsModal"; +import { ErrorModal } from "../Modal/ErrorModal"; +import { PasswordModal } from "../Modal/PasswordModal"; +import { ScreenShareModal } from "../Modal/ScreenShareModal"; +import { FileShareModal } from "../Modal/FileShareModal"; +import firebase from "firebase/compat/app"; +import { SubtitleModal } from "../Modal/SubtitleModal"; +import { HTML } from "./HTML"; +import { YouTube } from "./YouTube"; +import styles from "./App.module.css"; +import config from "../../config"; +import { MetadataContext } from "../../MetadataContext"; +import ChatVideoCard from "../ChatVideoCard/ChatVideoCard"; +import { ActionIcon, Badge, TextInput, Button } from "@mantine/core"; +import { + IconAntennaBars5, + IconBrowser, + IconChevronLeft, + IconChevronRight, + IconFile, + IconKeyboardFilled, + IconList, + IconScreenShare, + IconSettings, + IconUser, + IconUserScreen, + IconUsersGroup, + IconVolume, + IconX, +} from "@tabler/icons-react"; +import { InviteButton } from "../InviteButton/InviteButton"; +import type WebTorrent from "webtorrent"; +import type Hls from "hls.js"; +import { type MediaPlayerClass } from "dashjs"; +import { type Torrent } from "webtorrent"; + +declare global { + interface Window { + onYouTubeIframeAPIReady: any; + YT: YT.JsApi; + watchparty: { + ourStream: MediaStream | undefined; + videoRefs: HTMLVideoElementDict; + videoPCs: PCDict; + webtorrent?: WebTorrent.Instance; + hls?: Hls; + dash?: MediaPlayerClass; + }; + } +} + +window.watchparty = { + ourStream: undefined, + videoRefs: {}, + videoPCs: {}, +}; + +const clientId = getOrCreateClientId(); + +interface AppProps { + vanity?: string; + urlRoomId?: string; +} + +interface AppState { + state: "starting" | "connected"; + roomMedia: string; + roomSubtitle: string; + roomPaused: boolean; + roomLoop: boolean; + participants: User[]; + rosterUpdateTS: Number; + chat: ChatMessage[]; + playlist: PlaylistVideo[]; + tsMap: NumberDict; + nameMap: StringDict; + pictureMap: StringDict; + myName: string; + myPicture: string; + loading: boolean; + scrollTimestamp: number; + unreadCount: number; + fullScreen: boolean; + controlsTimestamp: number; + watchOptions: SearchResult[]; + isVBrowser: boolean; + isAutoPlayable: boolean; + downloaded: number; + total: number; + speed: number; + connections: number; + fileSelection: { + name: string; + url: string; + length: number; + playFn?: () => void; + }[]; + overlayMsg: string; + isErrorAuth: boolean; + settings: Settings; + vBrowserResolution: string; + vBrowserQuality: string; + isVBrowserLarge: boolean; + nonPlayableMedia: boolean; + currentTab: string; + isSubscribeModalOpen: boolean; + isVBrowserModalOpen: boolean; + isScreenShareModalOpen: boolean; + isFileShareModalOpen: boolean; + isSubtitleModalOpen: boolean; + isMultiSelectModalOpen: boolean; + roomLock: string; + controller?: string; + savedPasswords: StringDict; + roomId: string; + errorMessage: string; + successMessage: string; + warningMessage: string; + isChatDisabled: boolean; + showChatColumn: boolean; + showPeopleColumn: boolean; + owner: string | undefined; + vanity: string | undefined; + password: string | undefined; + inviteLink: string; + roomTitle: string | undefined; + roomDescription: string | undefined; + roomTitleColor: string | undefined; + mediaPath: string | undefined; + roomPlaybackRate: number; + isLiveStream: boolean; + settingsModalOpen: boolean; + uploadController: AbortController | undefined; +} + +export class App extends React.Component { + static contextType = MetadataContext; + declare context: React.ContextType; + state: AppState = { + state: "starting", + roomMedia: "", + roomPaused: false, + roomSubtitle: "", + roomLoop: false, + participants: [], + rosterUpdateTS: Date.now(), + chat: [], + playlist: [], + tsMap: {}, + nameMap: {}, + pictureMap: {}, + myName: window.localStorage.getItem("watchparty-username") ?? "", + myPicture: "", + loading: true, + scrollTimestamp: 0, + unreadCount: 0, + fullScreen: false, + controlsTimestamp: 0, + watchOptions: [], + isVBrowser: false, + isAutoPlayable: true, + downloaded: 0, + total: 0, + speed: 0, + connections: 0, + fileSelection: [], + overlayMsg: "", + isErrorAuth: false, + settings: {}, + vBrowserResolution: "1280x720@30", + vBrowserQuality: "1", + isVBrowserLarge: false, + nonPlayableMedia: false, + currentTab: + new URLSearchParams(window.location.search).get("tab") ?? "chat", + isSubscribeModalOpen: false, + isVBrowserModalOpen: false, + isScreenShareModalOpen: false, + isFileShareModalOpen: false, + isSubtitleModalOpen: false, + isMultiSelectModalOpen: false, + roomLock: "", + controller: "", + roomId: "", + savedPasswords: {}, + errorMessage: "", + successMessage: "", + warningMessage: "", + isChatDisabled: false, + showChatColumn: isMobile() + ? true + : Boolean( + Number( + window.localStorage.getItem("watchparty-showchatcolumn") ?? "1", + ), + ), + showPeopleColumn: false, + // Boolean( + // Number( + // window.localStorage.getItem('watchparty-showpeoplecolumn') ?? '0', + // ), + // ), + owner: undefined, + vanity: undefined, + password: undefined, + inviteLink: "", + roomTitle: "", + roomDescription: "", + roomTitleColor: "", + mediaPath: undefined, + roomPlaybackRate: 0, + isLiveStream: false, + settingsModalOpen: false, + uploadController: undefined, + }; + socket: Socket = null!; + mediasoupPubSocket: Socket | null = null; + mediasoupSubSocket: Socket | null = null; + ytDebounce = true; + localStreamToPublish?: MediaStream; + isLocalStreamAFile = false; + publisherConns: PCDict = {}; + consumerConn?: RTCPeerConnection; + progressUpdater?: number; + heartbeat: number | undefined = undefined; + YouTubeInterface: YouTube = new YouTube(null); + HTMLInterface: HTML = new HTML("leftVideo"); + Player = () => { + if (this.usingYoutube()) { + return this.YouTubeInterface; + } else { + return this.HTMLInterface; + } + }; + + chatRef = React.createRef(); + + async componentDidMount() { + document.onfullscreenchange = this.onFullScreenChange; + document.onkeydown = this.onKeydown; + + // Send heartbeat to the server + this.heartbeat = window.setInterval( + () => { + fetch(serverPath + "/ping"); + }, + 10 * 60 * 1000, + ); + + const canAutoplay = await testAutoplay(); + this.setState({ isAutoPlayable: canAutoplay }); + this.loadSettings(); + this.loadYouTube(); + this.init(); + if (config.VITE_FIREBASE_CONFIG) { + firebase.auth().onAuthStateChanged(async (user: firebase.User | null) => { + if (user) { + this.loadSignInData(user); + } + }); + } + } + + componentWillUnmount() { + document.removeEventListener("fullscreenchange", this.onFullScreenChange); + document.removeEventListener("keydown", this.onKeydown); + window.clearInterval(this.heartbeat); + } + + init = async () => { + let roomId = "/" + this.props.urlRoomId; + // if a vanity name, resolve the url to a room id + if (this.props.vanity) { + const resp = await fetch( + serverPath + "/resolveRoom/" + this.props.vanity, + ); + if (!resp.ok) { + this.setState({ overlayMsg: "Couldn't load this room." }); + return; + } + const data = await resp.json(); + if (!data?.roomId) { + this.setState({ overlayMsg: "Couldn't load this room." }); + return; + } + roomId = data.roomId; + } + this.setState({ roomId }, () => { + this.join(roomId); + }); + }; + + join = async (roomId: string) => { + const password = getSavedPasswords()[roomId] ?? ""; + const response = await fetch(serverPath + "/resolveShard" + roomId); + const shard = Number(await response.text()) || ""; + const socket = io(serverPath + roomId, { + transports: ["websocket"], + query: { + clientId, + password, + shard, + roomId: roomId.slice(1), + }, + auth: { + sessionId: getOrCreateSessionId(), + }, + }); + this.socket = socket; + socket.on("connect", async () => { + this.setState({ + state: "connected", + overlayMsg: "", + errorMessage: "", + successMessage: "", + warningMessage: "", + }); + // Use the name in our state, generate one if empty + this.updateName(this.state.myName || (await generateName())); + this.loadSignInData(this.context.user); + // Re-join video chat if we were in it before the reconnection + if (window.watchparty.ourStream) { + socket.emit("CMD:joinVideo"); + } + }); + socket.on("connect_error", (err: any) => { + console.error(err); + if (err.message === "Invalid namespace") { + this.setState({ overlayMsg: "Couldn't load this room." }); + } else if (err.message === "password") { + this.setState({ isErrorAuth: true }); + } else { + this.setState({ overlayMsg: err?.message ?? "An error occurred" }); + } + }); + socket.on("disconnect", (reason) => { + if (reason === "io server disconnect") { + // the disconnection was initiated by the server, you need to reconnect manually + this.setState({ overlayMsg: "Disconnected from server." }); + } else { + // else the socket will automatically try to reconnect + // Use the alert pill since it's less disruptive + this.setState({ warningMessage: "Reconnecting..." }); + } + }); + socket.on("errorMessage", (err: string) => { + this.setState({ errorMessage: err }); + setTimeout(() => { + this.setState({ errorMessage: "" }); + }, 3000); + }); + socket.on("successMessage", (success: string) => { + this.setState({ successMessage: success }); + setTimeout(() => { + this.setState({ successMessage: "" }); + }, 3000); + }); + socket.on("kicked", () => { + window.location.assign("/"); + }); + socket.on("REC:play", () => { + this.localPlay(); + }); + socket.on("REC:pause", () => { + this.localPause(); + }); + socket.on("REC:seek", (data: number) => { + this.localSeek(data); + }); + socket.on("REC:playbackRate", (data: number) => { + this.setState({ roomPlaybackRate: data }); + if (data > 0) { + this.Player().setPlaybackRate(data); + } + }); + socket.on("REC:subtitle", (data: string) => { + this.setState({ roomSubtitle: data }, () => { + this.Player().loadSubtitles(data); + }); + }); + socket.on("REC:loop", (data: boolean) => { + this.setState({ roomLoop: data }); + }); + socket.on("REC:changeController", (data: string) => { + this.setState({ controller: data }); + }); + socket.on("REC:host", async (data: HostState) => { + let currentMedia = data.video || ""; + if (this.playingScreenShare() && !isScreenShare(currentMedia)) { + this.stopPublishingLocalStream(); + } + if (this.playingFileShare() && !isFileShare(currentMedia)) { + this.stopPublishingLocalStream(); + } + if (this.playingVBrowser() && !isVBrowser(currentMedia)) { + this.stopVBrowser(); + } + if (this.playingScreenShare() && isScreenShare(currentMedia)) { + // Ignore, it's probably a reconnection + return; + } + if (this.playingFileShare() && isFileShare(currentMedia)) { + // Ignore, it's probably a reconnection + return; + } + if ( + this.playingVBrowser() && + this.getVBrowserHost() && + isVBrowser(currentMedia) + ) { + // Ignore, it's probably a reconnection + return; + } + this.setState( + { + roomMedia: currentMedia, + roomPaused: data.paused, + roomSubtitle: data.subtitle, + roomLoop: data.loop, + roomPlaybackRate: data.playbackRate, + loading: Boolean(data.video), + nonPlayableMedia: false, + isVBrowserLarge: data.isVBrowserLarge, + vBrowserResolution: "1280x720@30", + vBrowserQuality: "1", + controller: data.controller, + isLiveStream: false, + }, + async () => { + const leftVideo = this.HTMLInterface.getVideoEl(); + + // Stop all players + // Unless the user is sharing a file, because we play it in leftVideo and capture stream + if (!this.isLocalStreamAFile) { + this.HTMLInterface.pauseVideo(); + } + this.YouTubeInterface.stopVideo(); + + if (!this.isLocalStreamAFile) { + this.Player().clearState(); + } + if (data.subtitle) { + this.Player().loadSubtitles(data.subtitle); + } + if (data.playbackRate) { + this.Player().setPlaybackRate(data.playbackRate); + } + + if ( + this.playingScreenShare() || + this.playingFileShare() || + this.playingVBrowser() + ) { + console.log( + "exiting REC:host since we are using webRTC (fileshare, screenshare, or vbrowser). Check setupRTCConnections()", + ); + if (!(this.playingVBrowser() && !this.getVBrowserHost())) { + // Remove the loader unless we're waiting for a vbrowser + this.setLoadingFalse(); + } + return; + } + if (this.usingYoutube() && !this.YouTubeInterface.isReady()) { + console.log( + "YT player not ready, onReady callback will retry when it is", + ); + return; + } + const src = data.video; + const time = data.videoTS; + if (isMagnet(src)) { + // WebTorrent + if (!window.watchparty.webtorrent) { + const WebTorrent = //@ts-expect-error + (await import("webtorrent/dist/webtorrent.min.js")).default; + window.watchparty.webtorrent = new WebTorrent(); + const reg = await navigator.serviceWorker?.register("/sw.min.js"); + const worker = reg.active || reg.waiting || reg.installing; + const checkState = (worker: ServiceWorker | null) => { + if (worker?.state === "activated") { + return window.watchparty.webtorrent?.createServer({ + controller: reg, + }); + } + return null; + }; + if (!checkState(worker)) { + worker?.addEventListener("statechange", ({ target }) => + checkState(target as ServiceWorker), + ); + } + } + await new Promise(async (resolve) => { + const finish = (torrent: Torrent) => { + // Got torrent metadata! + console.log("Client is downloading:", torrent.infoHash); + + // Torrents can contain many files. + const files = torrent.files; + const fileIndex = new URLSearchParams(src).get("fileIndex"); + // Try to find a single large file to play + let target; + if (fileIndex != null && fileIndex !== "") { + target = files[Number(fileIndex)]; + } + if (!target) { + // Open the selector + // Selecting a file sets a new URL with the fileIndex set so we go through again + this.setMultiSelectModal(true); + this.setFileSelection( + files.map((f: WebTorrent.TorrentFile, i: number) => ({ + name: f.name, + url: src + `&fileIndex=${i}`, + length: f.length, + })), + ); + } else { + //@ts-expect-error + target.streamTo(leftVideo); + } + resolve(undefined); + }; + let target = await window.watchparty.webtorrent?.get(src); + if (!target) { + target = window.watchparty.webtorrent?.add(src, { + announce: [ + "wss://tracker.btorrent.xyz", + "wss://tracker.openwebtorrent.com", + ], + destroyStoreOnDestroy: true, + maxWebConns: 4, + path: "/tmp/webtorrent/", + storeCacheSlots: 20, + strategy: "sequential", + // noPeersIntervalTime: 30, + }); + } + if (target?.ready) { + finish(target); + } else { + target?.on("ready", () => { + finish(target); + }); + } + }); + } else if (isDash(src)) { + if (!window.watchparty.dash) { + const Dash = await import("dashjs"); + window.watchparty.dash = Dash.MediaPlayer().create(); + window.watchparty.dash.on("streamInitialized", (_e: any) => { + // for a live stream: + // html.currenttime is time since stream start + // html.duration is infinite + // player.duration is the seekable range + const isLiveStream = this.Player().getDuration() >= Infinity; + console.log("DASH stream initialized: isLive %s", isLiveStream); + this.setState({ + isLiveStream, + }); + }); + } + window.watchparty.dash.initialize(leftVideo, src); + } else if (isHls(src) && window.MediaSource) { + // Prefer using hls.js if MediaSource Extensions are supported + // otherwise fallback to native HLS support using video tag (i.e. iPhones) + if (!window.watchparty.hls) { + const Hls = (await import("hls.js")).default; + window.watchparty.hls = new Hls(); + window.watchparty.hls.on(Hls.Events.LEVEL_LOADED, (_, data) => { + const isLiveStream = data.details.live; + this.setState({ isLiveStream }); + console.log("HLS level loaded: isLive %s", isLiveStream); + }); + } + window.watchparty.hls.loadSource(src); + window.watchparty.hls.attachMedia(leftVideo); + } + // else if (isMpegTs(src)) { + // const mpegts = (await import('mpegts.js')).default; + // let player = mpegts.createPlayer({ + // type: 'mse', // could also be mpegts, m2ts, flv + // // isLive: true, + // url: src, + // }); + // player.attachMediaElement(leftVideo); + // player.load(); + // player.play(); + // } + else { + await this.Player().setSrcAndTime(src, time); + } + // Start this video + if (!data.paused) { + this.localPlay(); + } + // Do right before playing + leftVideo?.addEventListener( + "canplay", + () => { + this.setLoadingFalse(); + let ts = undefined; + // WebTorrent and Hls and Dash reset position back to 0 so set it back here + if ( + isMagnet(src) || + isHls(src) || + isDash(src) || + this.state.isLiveStream + ) { + ts = time; + } + // Resync to leader since the loading might have taken some time + this.localSeek(ts); + if (this.state.uploadController) { + // Jump back to the start of the video + this.roomSeek(0); + } + if (data.playbackRate) { + // Set playback rate again since it might have been lost + console.log("setting playback rate again", data.playbackRate); + this.Player().setPlaybackRate(data.playbackRate); + } + }, + { once: true }, + ); + + // Progress updater + window.clearInterval(this.progressUpdater); + this.setState({ downloaded: 0, total: 0, speed: 0 }); + if (currentMedia.includes("/stream?torrent=magnet")) { + this.progressUpdater = window.setInterval(async () => { + const response = await fetch( + currentMedia.replace("/stream", "/progress"), + ); + const data = await response.json(); + this.setState({ + downloaded: data.downloaded, + total: data.total, + speed: data.speed, + connections: data.connections, + }); + }, 1000); + } + if (isMagnet(currentMedia)) { + this.progressUpdater = window.setInterval(async () => { + const client = window.watchparty.webtorrent; + if (client) { + this.setState({ + downloaded: client.torrents[0]?.downloaded, + total: client.torrents[0]?.length, + speed: client.torrents[0]?.downloadSpeed, + connections: client.torrents[0]?.numPeers, + }); + } + }, 1000); + } + }, + ); + }); + socket.on("REC:chat", (data: ChatMessage) => { + if ( + !getCurrentSettings().disableChatSound && + !data.system && + ((document.visibilityState && document.visibilityState !== "visible") || + this.state.currentTab !== "chat") + ) { + new Audio("/clearly.mp3").play(); + } + this.state.chat.push(data); + if (this.state.chat.length > 100) { + this.state.chat.shift(); + } + this.setState({ + chat: this.state.chat, + scrollTimestamp: Date.now(), + unreadCount: + this.state.currentTab === "chat" + ? this.state.unreadCount + : this.state.unreadCount + 1, + }); + }); + socket.on("REC:addReaction", (data: Reaction) => { + const { chat } = this.state; + const msgIndex = chat.findIndex( + (m) => m.id === data.msgId && m.timestamp === data.msgTimestamp, + ); + if (msgIndex === -1) { + return; + } + const msg = chat[msgIndex]; + msg.reactions = msg.reactions || {}; + msg.reactions[data.value] = msg.reactions[data.value] || []; + msg.reactions[data.value].push(data.user); + this.setState({ chat }, () => { + // if we add a reaction to the last message we need to scroll down + // or else the reaction icon might be hidden + if ( + msgIndex === chat.length - 1 && + this.chatRef.current?.state.isNearBottom + ) { + this.chatRef.current?.scrollToBottom(); + } + }); + }); + socket.on("REC:removeReaction", (data: Reaction) => { + const { chat } = this.state; + const msg = chat.find( + (m) => m.id === data.msgId && m.timestamp === data.msgTimestamp, + ); + if (!msg || !msg.reactions?.[data.value]) { + return; + } + msg.reactions[data.value] = msg.reactions[data.value].filter( + (id) => id !== data.user, + ); + this.setState({ chat }); + }); + socket.on("REC:tsMap", (data: NumberDict) => { + this.setState({ tsMap: data }, () => { + // Dynamic playback rate based on timestamps + // Disable for sharing types where the users can have different timestamps + // e.g. screenshare, fileshare, .m3u8 HLS streams + // Also not necessary for WebRTC sharing since it should be close to realtime + if ( + !this.state.roomPaused && + !this.state.isLiveStream && + this.hasDuration() && + this.state.roomPlaybackRate === 0 + ) { + const leader = this.getLeaderTime(); + const delta = leader - data[clientId]; + // Set leader pbr to 1 + let pbr = 1; + // Add .01 pbr for each 100ms delay + if (delta > 0.5) { + pbr += Number((delta / 10).toFixed(2)); + pbr = Math.min(pbr, 1.1); + } + // console.log(delta, pbr); + if (this.Player().getPlaybackRate() !== pbr) { + this.Player().setPlaybackRate(pbr); + } + } + if (this.state.roomSubtitle) { + const sharer = this.state.participants.find((p) => p.isScreenShare); + if (sharer && sharer.id !== clientId) { + // Sync only if someone is sharing and it's not us + const sharerTime = this.state.tsMap[sharer.id]; + this.Player().syncSubtitles(sharerTime); + } + } + }); + }); + socket.on("REC:nameMap", (data: StringDict) => { + this.setState({ nameMap: data }); + }); + socket.on("REC:pictureMap", (data: StringDict) => { + this.setState({ pictureMap: data }); + }); + socket.on("REC:lock", (data: string) => { + this.setState({ roomLock: data }); + }); + socket.on("roster", (data: User[]) => { + this.setState({ participants: data, rosterUpdateTS: Date.now() }, () => { + this.setupRTCConnections(); + }); + }); + socket.on("chatinit", (data: ChatMessage[]) => { + this.setState({ chat: data, scrollTimestamp: Date.now() }); + }); + socket.on("playlist", (data: PlaylistVideo[]) => { + this.setState({ playlist: data }); + }); + socket.on( + "signalSS", + async (data: { + msg: { ice: any; sdp: any }; + from: string; + sharer: boolean; + }) => { + config.NODE_ENV === "development" && console.log(data); + // Handle messages received from signaling server + const msg = data.msg; + const from = data.from; + // Determine whether the message came from the sharer or the sharee + const pc = ( + data.sharer ? this.consumerConn : this.publisherConns[from] + ) as RTCPeerConnection; + if (msg.ice !== undefined) { + pc.addIceCandidate(new RTCIceCandidate(msg.ice)); + } else if (msg.sdp && msg.sdp.type === "offer") { + // console.log('offer'); + // TODO Currently ios/Safari cannot handle this property, so remove it from the offer + const _sdp = msg.sdp.sdp + .split("\n") + .filter((line: string) => { + return line.trim() !== "a=extmap-allow-mixed"; + }) + .join("\n"); + msg.sdp.sdp = _sdp; + await pc.setRemoteDescription(new RTCSessionDescription(msg.sdp)); + const answer = await pc.createAnswer(); + // Allow stereo audio + answer.sdp = answer.sdp?.replace( + "useinbandfec=1", + "useinbandfec=1; stereo=1; maxaveragebitrate=510000", + ); + // console.log(answer.sdp); + // Allow multichannel audio if Chromium + //@ts-expect-error + const isChromium = Boolean(window.chrome); + if (isChromium) { + answer.sdp = answer.sdp + ?.replace("opus/48000/2", "multiopus/48000/6") + .replace( + "useinbandfec=1", + "channel_mapping=0,4,1,2,3,5; num_streams=4; coupled_streams=2;maxaveragebitrate=510000;minptime=10;useinbandfec=1", + ); + } + await pc.setLocalDescription(answer); + this.sendSignalSS(from, { sdp: pc.localDescription }, !data.sharer); + } else if (msg.sdp && msg.sdp.type === "answer") { + pc.setRemoteDescription(new RTCSessionDescription(msg.sdp)); + } + }, + ); + socket.on("REC:getRoomState", this.handleRoomState); + window.setInterval(() => { + if (this.state.roomMedia) { + const toSend = this.getRoomTSToSet(this.Player().getCurrentTime()); + this.socket.emit("CMD:ts", toSend); + } + }, 1000); + }; + + setFileSelection = ( + data: { name: string; url: string; length: number; playFn?: () => void }[], + ) => { + this.setState({ fileSelection: data }); + }; + + setMultiSelectModal = (isMultiSelectModalOpen: boolean) => { + this.setState({ isMultiSelectModalOpen }); + }; + + resetMultiSelect = () => { + this.setState({ isMultiSelectModalOpen: false, fileSelection: [] }); + }; + + loadSettings = async () => { + // Load settings from localstorage + let settings = getCurrentSettings(); + this.setState({ settings }); + }; + + loadSignInData = async (user: firebase.User | undefined) => { + if (user && this.socket) { + // NOTE: firebase auth doesn't provide the actual first name data that individual providers (G/FB) do + // It's accessible at the time the user logs in but not afterward + // If we want accurate surname/given name we'll need to save that somewhere + const firstName = user.displayName?.split(" ")[0]; + if (firstName) { + // Don't update the username if the user wants to customize their own + // Set a flag in localstorage so we only update this once, if the user changes name manually later we won't overwrite + // Clear the flag on logout + if (!window.localStorage.getItem("watchparty-loginname")) { + this.updateName(firstName); + window.localStorage.setItem( + "watchparty-loginname", + Date.now().toString(), + ); + } + } + const userImage = await getUserImage(user); + if (userImage) { + this.updatePicture(userImage); + } + this.updateUid(user); + } + }; + + loadYouTube = () => { + // This code loads the IFrame Player API code asynchronously. + const tag = document.createElement("script"); + tag.src = "https://www.youtube.com/iframe_api"; + document.body.append(tag); + window.onYouTubeIframeAPIReady = () => { + // Note: this fails silently if the element is not available + const ytPlayer = new window.YT.Player("leftYt", { + events: { + onReady: () => { + console.log("yt onReady"); + this.YouTubeInterface = new YouTube(ytPlayer); + this.setState({ loading: false }); + // We might have failed to play YT originally, ask for the current video again + if (this.usingYoutube()) { + console.log("requesting host data again after ytReady"); + this.socket.emit("CMD:askHost"); + } + }, + onStateChange: (e) => { + if ( + this.usingYoutube() && + e.data === window.YT?.PlayerState?.CUED + ) { + this.setState({ loading: false }); + } + if ( + this.usingYoutube() && + e.data === window.YT?.PlayerState?.ENDED + ) { + console.log(e.data, e.target.getVideoUrl()); + this.onVideoEnded(e.target.getVideoUrl()); + } + if ( + this.ytDebounce && + ((e.data === window.YT?.PlayerState?.PLAYING && + this.state.roomPaused) || + (e.data === window.YT?.PlayerState?.PAUSED && + !this.state.roomPaused)) + ) { + this.ytDebounce = false; + if (e.data === window.YT?.PlayerState?.PLAYING) { + this.socket.emit("CMD:play"); + this.localPlay(); + } else { + this.socket.emit("CMD:pause"); + this.localPause(); + } + window.setTimeout(() => (this.ytDebounce = true), 500); + } + }, + }, + }); + }; + }; + + // Functions for managing room settings + getInviteLink = (vanity: string) => { + if (vanity) { + return `${window.location.origin}/r/${vanity}`; + } + return `${window.location.origin}/watch${this.state.roomId}`; + }; + + handleRoomState = (data: any) => { + this.setOwner(data.owner); + this.setVanity(data.vanity); + this.setPassword(data.password); + this.setInviteLink(this.getInviteLink(data.vanity)); + this.setIsChatDisabled(data.isChatDisabled); + this.setRoomTitle(data.roomTitle); + this.setRoomDescription(data.roomDescription); + this.setRoomTitleColor(data.roomTitleColor); + this.setMediaPath(data.mediaPath); + window.history.replaceState("", "", this.getInviteLink(data.vanity)); + }; + + setOwner = (owner: string) => { + this.setState({ owner }); + }; + setVanity = (vanity: string | undefined) => { + this.setState({ vanity }); + }; + setPassword = (password: string | undefined) => { + this.setState({ password }); + }; + setInviteLink = (inviteLink: string) => { + this.setState({ inviteLink }); + }; + setRoomTitle = (roomTitle: string | undefined) => { + this.setState({ roomTitle }); + }; + setRoomDescription = (roomDescription: string | undefined) => { + this.setState({ roomDescription }); + }; + setRoomTitleColor = (roomTitleColor: string | undefined) => { + this.setState({ roomTitleColor }); + }; + setMediaPath = (mediaPath: string | undefined) => { + this.setState({ mediaPath }); + }; + + setRoomLock = async (locked: boolean) => { + this.socket.emit("CMD:lock", { locked }); + }; + + haveLock = () => { + if (!this.state.roomLock) { + return true; + } + return this.context.user?.uid === this.state.roomLock; + }; + + setIsChatDisabled = (val: boolean) => this.setState({ isChatDisabled: val }); + + clearChat = async () => { + this.socket.emit("CMD:deleteChatMessages", {}); + }; + + startConvert = async (sourceUrl?: string) => { + let stream = new ReadableStream(); + let file: File; + if (!sourceUrl) { + const files = await openFileSelector(); + if (!files) { + return; + } + file = files[0]; + // Start uploading stream + stream = file.stream(); + } + const uuid = createUuid(); + const convertPath = this.context.convertPath; + // const convertPath = 'https://azure.howardchung.net:5001'; + let convertUrl = convertPath + "/" + uuid + ".m3u8"; + convertUrl += sourceUrl ? "?url=" + encodeURIComponent(sourceUrl) : ""; + // Wait for the playlist to get generated + const poll = async () => { + let ok = false; + let i = 0; + while (!ok && i < 30) { + await new Promise((resolve) => setTimeout(resolve, 2000)); + const resp = await fetch(convertUrl); + ok = resp.ok; + i += 1; + } + // Same URL but GET + this.roomSetMedia(convertUrl); + }; + poll(); + const reader = stream.getReader(); + const start = Date.now(); + let bytes = 0; + const ws = new WebSocket(convertUrl.replace("http", "ws")); + ws.onmessage = async (_ev) => { + // Server sends a message whenever it wants next chunk + const { done, value } = await reader.read(); + if (value) { + ws.send(value); + } + if (done) { + ws.close(); + } + const end = Date.now(); + bytes += value?.length ?? 0; + this.setState({ + downloaded: bytes, + total: file?.size, + speed: done ? 0 : bytes / ((end - start) / 1000), + connections: 1, + }); + }; + ws.onclose = () => { + this.setState({ uploadController: undefined }); + }; + const controller = new AbortController(); + controller.signal.onabort = (_ev) => { + ws.close(); + }; + this.setState({ + uploadController: controller, + }); + // Note: If using fetch we can't read the response until the request completes + // await fetch(convertUrl, { + // method: 'POST', + // body: stream, + // signal: this.state.uploadController?.signal, + // //@ts-expect-error + // duplex: 'half', + // }); + }; + + startFileShare = async (useMediaSoup: boolean) => { + const files = await openFileSelector(); + if (!files) { + return; + } + const file = files[0]; + this.Player().clearState(); + const leftVideo = this.HTMLInterface.getVideoEl(); + leftVideo.src = URL.createObjectURL(file); + leftVideo.play(); + //@ts-expect-error + this.localStreamToPublish = leftVideo?.captureStream(); + this.isLocalStreamAFile = true; + if (this.localStreamToPublish) { + this.socket.emit("CMD:joinScreenShare", { + file: true, + mediasoup: useMediaSoup, + }); + } + }; + + startScreenShare = async (useMediaSoup: boolean) => { + if (navigator.mediaDevices.getDisplayMedia) { + const stream = await navigator.mediaDevices.getDisplayMedia({ + //@ts-expect-error + video: { height: 720, logicalSurface: true }, + audio: { + autoGainControl: false, + channelCount: 2, + echoCancellation: false, + noiseSuppression: false, + sampleRate: 48000, + sampleSize: 16, + }, + }); + this.localStreamToPublish = stream; + this.isLocalStreamAFile = false; + this.socket.emit("CMD:joinScreenShare", { + file: false, + mediasoup: useMediaSoup, + }); + } + }; + + // Share the video to mediasoup + publishMediasoup = async (mediasoupURL: string) => { + const localStream = this.localStreamToPublish; + let device: MediasoupClient.types.Device = null as any; + let producerTransport: MediasoupClient.types.Transport = null as any; + + // =========== socket.io ========== + const connectSocket = (mediasoupURL: string) => { + return new Promise((resolve, reject) => { + this.mediasoupPubSocket = io(mediasoupURL, { + transports: ["websocket"], + }); + + const socket = this.mediasoupPubSocket; + socket?.on("connect", function () { + console.log("PUBLISH: connected to socket.io"); + resolve(); + }); + socket?.on("error", function (err) { + console.error("PUBLISH: socket.io ERROR:", err); + reject(err); + }); + }); + }; + + const sendRequest = (type: string, data: any) => { + return new Promise((resolve, reject) => { + const socket = this.mediasoupPubSocket; + socket?.emit(type, data, (err: any, response: any) => { + if (!err) { + // Success response, so pass the mediasoup response to the local Room. + resolve(response); + } else { + reject(err); + } + }); + }); + }; + + async function publish() { + // --- get transport info --- + console.log("PUBLISH: --- createProducerTransport --"); + const params = await sendRequest("createProducerTransport", {}); + console.log("PUBLISH: transport params:", params); + producerTransport = device.createSendTransport(params); + console.log("PUBLISH: createSendTransport:", producerTransport); + + // --- join & start publish -- + producerTransport.on( + "connect", + async ( + { + dtlsParameters, + }: { dtlsParameters: MediasoupClient.types.DtlsParameters }, + callback: () => void, + errback: (error: Error) => void, + ) => { + console.log("PUBLISH: --transport connect"); + sendRequest("connectProducerTransport", { + dtlsParameters: dtlsParameters, + }) + .then(callback) + .catch(errback); + }, + ); + + producerTransport.on( + "produce", + async ( + { + kind, + rtpParameters, + }: { + kind: string; + rtpParameters: MediasoupClient.types.RtpParameters; + }, + callback: ({ id }: { id: string }) => void, + errback: (error: Error) => void, + ) => { + console.log("PUBLISH: --transport produce"); + try { + const { id } = await sendRequest("produce", { + transportId: producerTransport.id, + kind, + rtpParameters, + }); + callback({ id }); + } catch (err: any) { + errback(err); + } + }, + ); + + // producerTransport.on('connectionstatechange', (state: string) => { + // switch (state) { + // case 'connecting': + // console.log('PUBLISH: connecting'); + // break; + + // case 'connected': + // console.log('PUBLISH: connected'); + // break; + + // case 'failed': + // console.log('PUBLISH: failed'); + // producerTransport.close(); + // break; + + // default: + // break; + // } + // }); + + const videoTrack = localStream?.getVideoTracks()[0]; + if (videoTrack) { + const trackParams = { track: videoTrack }; + await producerTransport.produce(trackParams); + } + const audioTrack = localStream?.getAudioTracks()[0]; + if (audioTrack) { + const trackParams = { track: audioTrack }; + await producerTransport.produce(trackParams); + } + } + + async function loadDevice( + routerRtpCapabilities: MediasoupClient.types.RtpCapabilities, + ) { + const { Device } = await import("mediasoup-client"); + device = new Device(); + await device.load({ routerRtpCapabilities }); + } + + await connectSocket(mediasoupURL); + // --- get capabilities -- + const data = await sendRequest("getRouterRtpCapabilities", {}); + console.log("PUBLISH: getRouterRtpCapabilities:", data); + await loadDevice(data); + await publish(); + }; + + // Play the video from MediaSoup + subscribeMediasoup = async (mediaSoupURL: string) => { + let device: MediasoupClient.types.Device = null as any; + let consumerTransport: MediasoupClient.types.Transport = null as any; + // =========== socket.io ========== + + const connectSocket = () => { + return new Promise((resolve, reject) => { + this.mediasoupSubSocket = io(mediaSoupURL, { + transports: ["websocket"], + }); + const socket = this.mediasoupSubSocket; + socket?.on("connect", function () { + console.log("SUBSCRIBE: connected to socket.io"); + resolve(); + }); + socket?.on("error", function (err) { + console.error("SUBSCRIBE: socket.io ERROR:", err); + reject(err); + }); + socket?.on("newProducer", async function (message) { + console.log("SUBSCRIBE: socket.io newProducer:", message); + if (consumerTransport) { + // start consume + if (message.kind === "video") { + await consumeAndResume(message.kind); + } else if (message.kind === "audio") { + await consumeAndResume(message.kind); + } + } + }); + + // socket?.on('producerClosed', function (message) { + // console.log('socket.io producerClosed:', message); + // const localId = message.localId; + // const remoteId = message.remoteId; + // const kind = message.kind; + // if (kind === 'video') { + // if (videoConsumer) { + // videoConsumer.close(); + // videoConsumer = null; + // } + // } else if (kind === 'audio') { + // if (audioConsumer) { + // audioConsumer.close(); + // audioConsumer = null; + // } + // } + // }); + }); + }; + + const sendRequest = (type: string, data: any) => { + return new Promise((resolve, reject) => { + const socket = this.mediasoupSubSocket; + socket?.emit(type, data, (err: Error, response: any) => { + if (!err) { + // Success response, so pass the mediasoup response to the local Room. + resolve(response); + } else { + reject(err); + } + }); + }); + }; + + // =========== media handling ========== + const addRemoteTrack = (track: MediaStreamTrack) => { + let video = this.HTMLInterface.getVideoEl(); + if (video.srcObject) { + // Track already exists, add it + (video.srcObject as MediaStream).addTrack(track); + } else { + const mediaStream = new MediaStream(); + mediaStream.addTrack(track); + video.srcObject = mediaStream; + } + this.localPlay(); + }; + + async function consumeAndResume(kind: string) { + const consumer = await consume(consumerTransport, kind); + if (consumer) { + console.log("SUBSCRIBE: -- track exist, consumer ready. kind=" + kind); + if (kind === "video") { + console.log("SUBSCRIBE: -- resume kind=" + kind); + sendRequest("resume", { kind: kind }) + .then(() => { + console.log("SUBSCRIBE: resume OK"); + return consumer; + }) + .catch((err) => { + console.error("SUBSCRIBE: resume ERROR:", err); + return consumer; + }); + } else { + console.log("SUBSCRIBE: -- do not resume kind=" + kind); + } + } else { + console.log("SUBSCRIBE: -- no consumer yet. kind=" + kind); + return null; + } + } + + async function loadDevice( + routerRtpCapabilities: MediasoupClient.types.RtpCapabilities, + ) { + try { + const { Device } = await import("mediasoup-client"); + device = new Device(); + await device.load({ routerRtpCapabilities }); + } catch (error: any) { + if (error.name === "UnsupportedError") { + console.error("browser not supported"); + } + } + } + + async function consume( + transport: MediasoupClient.types.Transport, + trackKind: string, + ) { + console.log("SUBSCRIBE: --start of consume --kind=" + trackKind); + const { rtpCapabilities } = device; + const data = await sendRequest("consume", { + rtpCapabilities: rtpCapabilities, + kind: trackKind, + }).catch((err) => { + console.error("SUBSCRIBE: ERROR:", err); + }); + const { producerId, id, kind, rtpParameters } = data; + + if (producerId) { + let codecOptions = {}; + const consumer = await transport.consume({ + id, + producerId, + kind, + rtpParameters, + //@ts-expect-error + codecOptions, + }); + + addRemoteTrack(consumer.track); + console.log("SUBSCRIBE: --end of consume"); + return consumer; + } else { + console.warn("SUBSCRIBE: ---remote producer NOT READY"); + return null; + } + } + + async function subscribe() { + console.log("SUBSCRIBE: ---createConsumerTransport --"); + const params = await sendRequest("createConsumerTransport", {}); + console.log("SUBSCRIBE: transport params:", params); + consumerTransport = device.createRecvTransport(params); + console.log("SUBSCRIBE: createConsumerTransport:", consumerTransport); + + // --- join & start watching + consumerTransport.on( + "connect", + async ( + { + dtlsParameters, + }: { dtlsParameters: MediasoupClient.types.DtlsParameters }, + callback: () => void, + errback: (err: Error) => void, + ) => { + console.log("SUBSCRIBE: ---consumer transport connect"); + sendRequest("connectConsumerTransport", { + dtlsParameters: dtlsParameters, + }) + .then(callback) + .catch(errback); + }, + ); + + // consumerTransport.on('connectionstatechange', (state: string) => { + // switch (state) { + // case 'connecting': + // console.log('SUBSCRIBE: connecting'); + // break; + + // case 'connected': + // console.log('SUBSCRIBE: connected'); + // break; + + // case 'failed': + // console.log('SUBSCRIBE: failed'); + // consumerTransport.close(); + // break; + + // default: + // break; + // } + // }); + + await consumeAndResume("video"); + await consumeAndResume("audio"); + } + + // Clear the srcobject so we load our stream when received + const leftVideo = this.HTMLInterface.getVideoEl(); + leftVideo.srcObject = null; + await connectSocket(); + // --- get capabilities -- + const data = await sendRequest("getRouterRtpCapabilities", {}); + console.log("getRouterRtpCapabilities:", data); + await loadDevice(data); + await subscribe(); + }; + + stopPublishingLocalStream = async () => { + if (this.localStreamToPublish) { + this.socket.emit("CMD:leaveScreenShare"); + // We don't actually need to unmute if it's a fileshare but this is fine + this.localSetMute(false); + } + this.localStreamToPublish && + this.localStreamToPublish.getTracks().forEach((track) => { + track.stop(); + }); + this.localStreamToPublish = undefined; + if (this.consumerConn) { + this.consumerConn.close(); + this.consumerConn = undefined; + } + Object.values(this.publisherConns).forEach((pc) => { + pc.close(); + }); + this.publisherConns = {}; + this.isLocalStreamAFile = false; + if (this.mediasoupPubSocket) { + this.mediasoupPubSocket.close(); + this.mediasoupPubSocket = null; + } + if (this.mediasoupSubSocket) { + this.mediasoupSubSocket.close(); + this.mediasoupSubSocket = null; + } + }; + + setupRTCConnections = async () => { + if (!this.playingScreenShare() && !this.playingFileShare()) { + return; + } + const sharer = this.state.participants.find((p) => p.isScreenShare); + const selfId = getOrCreateClientId(); + const localTrack = this.localStreamToPublish?.getVideoTracks()[0]; + if (localTrack && !localTrack.onended) { + // Stop sharing if the local stream stops + localTrack.onended = () => this.stopPublishingLocalStream(); + } + if (this.state.roomMedia.includes("@")) { + let prefix = "screenshare://"; + if (this.playingFileShare()) { + prefix = "fileshare://"; + } + const unprefixed = this.state.roomMedia.replace(prefix, ""); + const mediasoupURL = unprefixed.split("@")[1]; + if (sharer?.id === selfId && this.mediasoupPubSocket == null) { + await this.publishMediasoup(mediasoupURL); + } + // If we're not sharing a file, also start watching + // avoid duplicate watching if the socket already exists + if (!this.isLocalStreamAFile && this.mediasoupSubSocket == null) { + await this.subscribeMediasoup(mediasoupURL); + } + return; + } + + // We're the sharer, create a connection to each other member + if (sharer?.id === selfId) { + // Delete and close any connections that aren't in the current member list (maybe someone disconnected) + // This allows them to rejoin later + const clientIds = new Set(this.state.participants.map((p) => p.id)); + Object.entries(this.publisherConns).forEach(([key, value]) => { + if (!clientIds.has(key)) { + value.close(); + delete this.publisherConns[key]; + } + }); + + this.state.participants.forEach((user) => { + const id = user.id; + if (id === selfId && this.isLocalStreamAFile) { + // Don't set up a connection to ourselves if sharing file + return; + } + if (!this.publisherConns[id]) { + // Set up the RTCPeerConnection for sharing media to each member + const pc = new RTCPeerConnection({ iceServers: iceServers() }); + this.publisherConns[id] = pc; + this.localStreamToPublish?.getTracks().forEach((track) => { + if (this.localStreamToPublish != null) { + pc.addTrack(track, this.localStreamToPublish); + } + }); + pc.onicecandidate = (event) => { + // We generated an ICE candidate, send it to peer + if (event.candidate) { + this.sendSignalSS(id, { ice: event.candidate }, true); + } + }; + pc.onnegotiationneeded = async () => { + // Start connection for peer's video + const offer = await pc.createOffer(); + await pc.setLocalDescription(offer); + this.sendSignalSS(id, { sdp: pc.localDescription }, true); + }; + } + }); + } + // We're a watcher, establish connection to sharer + // If screensharing, sharer also does this + // If filesharing, sharer does not do this since we use leftVideo + if (sharer && !this.consumerConn && !this.isLocalStreamAFile) { + const pc = new RTCPeerConnection({ iceServers: iceServers() }); + this.consumerConn = pc; + pc.onicecandidate = (event) => { + // We generated an ICE candidate, send it to sharer + if (event.candidate) { + this.sendSignalSS(sharer.id, { ice: event.candidate }); + } + }; + pc.ontrack = (event: RTCTrackEvent) => { + // Mount the stream from sharer + // console.log(stream); + const leftVideo = this.HTMLInterface.getVideoEl(); + if (leftVideo) { + leftVideo.src = ""; + leftVideo.srcObject = event.streams[0]; + this.localPlay(); + } + }; + } + }; + + startVBrowser = async (options: { size: string }) => { + this.socket.emit("CMD:startVBrowser", { options }); + }; + + stopVBrowser = async () => { + this.socket.emit("CMD:stopVBrowser"); + }; + + changeController = async (value: string | null) => { + // console.log(data); + this.socket.emit("CMD:changeController", value); + }; + + sendSignalSS = async (to: string, data: any, sharer?: boolean) => { + // console.log('sendSS', to, data); + this.socket.emit("signalSS", { to, msg: data, sharer }); + }; + + usingYoutube = () => { + return isYouTube(this.state.roomMedia); + }; + + usingNative = () => { + // Anything that uses HTML Video (e.g. not YouTube, Vimeo, or other embedded JS player) + return !this.usingYoutube(); + }; + + hasDuration = () => { + // Youtube, link, or magnet, etc. Has a defined runtime (not WebRTC) + return isHttp(this.state.roomMedia) || isMagnet(this.state.roomMedia); + }; + + playingScreenShare = () => { + return isScreenShare(this.state.roomMedia); + }; + + playingFileShare = () => { + return isFileShare(this.state.roomMedia); + }; + + playingVBrowser = () => { + return isVBrowser(this.state.roomMedia); + }; + + getVBrowserPass = () => { + return this.state.roomMedia.replace("vbrowser://", "").split("@")[0]; + }; + + getVBrowserHost = () => { + return this.state.roomMedia.replace("vbrowser://", "").split("@")[1]; + }; + + isPauseDisabled = () => { + return this.playingScreenShare() || this.playingVBrowser(); + }; + + localSeek = (customTime?: number) => { + // Jump to the leader's position, or a custom one + let target = customTime ?? this.getLeaderTime(); + // For live this is the offset from the leading edge (negative) + if (this.state.isLiveStream) { + target = this.Player().getDuration() + (customTime ?? 0); + } + if (target >= 0 && target < Infinity) { + console.log("syncing self to leader or custom:", target); + this.Player().seekVideo(target); + } + }; + + localPlay = async () => { + if (!this.state.roomMedia) { + return; + } + const canAutoplay = this.state.isAutoPlayable || (await testAutoplay()); + this.setState( + { roomPaused: false, isAutoPlayable: canAutoplay }, + async () => { + if ( + !this.state.isAutoPlayable || + (this.localStreamToPublish && !this.isLocalStreamAFile) + ) { + console.log("auto-muting to allow autoplay or screenshare host"); + this.localSetMute(true); + } else { + this.localSetMute(false); + } + try { + await this.Player().playVideo(); + } catch (e: any) { + console.warn(e, e.name); + if (e.name === "NotSupportedError" && this.usingNative()) { + this.setState({ loading: false, nonPlayableMedia: true }); + } + } + }, + ); + }; + + localPause = () => { + this.setState({ roomPaused: true }, async () => { + this.Player().pauseVideo(); + }); + }; + + localSetMute = (muted: boolean) => { + this.Player().setMute(muted); + this.refreshControls(); + }; + + localSetVolume = (volume: number) => { + this.Player().setVolume(volume); + this.refreshControls(); + }; + + localSubtitleModal = () => { + // Native player uses subtitle modal. + if (this.usingNative()) { + this.setState({ isSubtitleModalOpen: true }); + } + }; + + roomSetPlaybackRate = (rate: number) => { + // emit an event to the server + this.socket.emit("CMD:playbackRate", rate); + }; + + roomSetLoop = (loop: boolean) => { + this.socket.emit("CMD:loop", loop); + }; + + roomTogglePlay = () => { + if (!this.haveLock()) { + return; + } + if (this.isPauseDisabled()) { + return; + } + const shouldPlay = this.Player().shouldPlay(); + if (shouldPlay) { + this.socket.emit("CMD:play"); + this.localPlay(); + } else { + this.socket.emit("CMD:pause"); + this.localPause(); + } + }; + + roomSeek = (time: number) => { + let target = time; + target = Math.max(target, 0); + this.Player().seekVideo(target); + const toSend = this.getRoomTSToSet(target); + this.socket.emit("CMD:seek", toSend); + }; + + getRoomTSToSet = (time: number) => { + let target = time; + // In live case, can't just send video time because clients may have different durations + // Take the passed time and compute the offset from the end time (negative) + // Each client should use this value to compute the time to set when starting playback + if (this.state.isLiveStream) { + target = time - this.Player().getDuration(); + } + // Otherwise just return the time + return target; + }; + + onFullScreenChange = () => { + this.setState({ fullScreen: Boolean(document.fullscreenElement) }); + setTimeout(() => this.chatRef.current?.scrollToBottom(), 100); + }; + + onKeydown = (e: any) => { + if (!document.activeElement || document.activeElement.tagName === "BODY") { + if (e.key === " ") { + e.preventDefault(); + this.roomTogglePlay(); + } else if (e.key === "ArrowRight") { + this.roomSeek(this.Player().getCurrentTime() + 10); + } else if (e.key === "ArrowLeft") { + this.roomSeek(this.Player().getCurrentTime() - 10); + } else if (e.key === "t") { + this.localFullScreen(false); + } else if (e.key === "f") { + this.localFullScreen(true); + } else if (e.key === "m") { + this.localToggleMute(); + } + } + }; + + localFullScreen = async (bVideoOnly: boolean) => { + // Default: fullscreen the body (theater mode) + let container = document.body as HTMLElement; + if (bVideoOnly || isMobile()) { + if (this.playingVBrowser() && !isMobile()) { + // vbrowser needs to fullscreen the control wrapper div + // Can't really control the VBrowser on mobile anyway, so just fullscreen the video + container = document.getElementById("leftVideoParent") as HTMLElement; + } else { + // fullscreen just the video + container = this.Player().getVideoEl(); + } + } + if ( + !container.requestFullscreen && + //@ts-expect-error + container.webkitEnterFullScreen + ) { + // e.g. iPhone doesn't allow requestFullscreen + //@ts-expect-error + container.webkitEnterFullscreen(); + return; + } + if (!document.fullscreenElement) { + // not currently in fullscreen + await container.requestFullscreen(); + } else { + // e.g. switching from video fullscreen to theater mode + const bChangeElements = document.fullscreenElement !== container; + await document.exitFullscreen(); + if (bChangeElements) { + await container.requestFullscreen(); + } + } + }; + + localToggleMute = () => { + this.localSetMute(!this.Player().isMuted()); + }; + + roomSetMedia = (value: string) => { + this.socket.emit("CMD:host", value); + }; + + roomPlaylistPlay = (index: number) => { + this.roomSetMedia(this.state.playlist[index]?.url); + this.roomPlaylistDelete(index); + }; + + roomPlaylistAdd = (value: string) => { + this.socket.emit("CMD:playlistAdd", value); + }; + + roomPlaylistMove = (index: number, toIndex: number) => { + this.socket.emit("CMD:playlistMove", { index, toIndex }); + }; + + roomPlaylistDelete = (index: number) => { + this.socket.emit("CMD:playlistDelete", index); + }; + + updateName = (name: string) => { + this.setState({ myName: name }); + this.socket.emit("CMD:name", name); + window.localStorage.setItem("watchparty-username", name); + }; + + updatePicture = (url: string) => { + this.setState({ myPicture: url }); + this.socket.emit("CMD:picture", url); + }; + + updateUid = async (user: firebase.User) => { + const uid = user.uid; + const token = await user.getIdToken(); + this.socket.emit("CMD:uid", { uid, token }); + }; + + getMediaDisplayName = (input?: string) => { + if (!input) { + return ""; + } + // Show the whole URL for youtube + if (this.usingYoutube()) { + return input; + } + if (input.startsWith("screenshare://")) { + const sharer = this.state.participants.find((user) => user.isScreenShare); + return this.state.nameMap[sharer?.id ?? ""] + "'s screen"; + } + if (input.startsWith("fileshare://")) { + const sharer = this.state.participants.find((user) => user.isScreenShare); + return this.state.nameMap[sharer?.id ?? ""] + "'s file"; + } + if (input.startsWith("vbrowser://")) { + return "Virtual Browser" + (this.state.isVBrowserLarge ? "+" : ""); + } + if (isMagnet(input)) { + const magnetParsed = new URLSearchParams(input); + const index = magnetParsed.get("fileIndex"); + return magnetParsed.get("dn") + (index != null ? ` (file ${index})` : ""); + } + if (input.includes("/stream?torrent=magnet")) { + const search = new URL(input).search; + const searchParsed = new URLSearchParams(search); + const magnetUrl = searchParsed.get("torrent") ?? ""; + const magnetParsed = new URLSearchParams(magnetUrl); + const index = searchParsed.get("fileIndex"); + return ( + (magnetParsed.get("dn") ?? searchParsed.get("dn")) + + (index != null ? ` (file ${index})` : "") + ); + } + if (input.includes("/proxy")) { + const urlParsed = new URLSearchParams(input); + const displayName = urlParsed.get("displayName"); + if (displayName) { + return displayName; + } + } + return input; + }; + + setLoadingFalse = () => { + this.setState({ loading: false }); + }; + + getLeaderTime = () => { + if (this.state.participants.length > 2) { + return calculateMedian(Object.values(this.state.tsMap)); + } + return Math.max(...Object.values(this.state.tsMap)); + }; + + onVideoEnded = (url: string) => { + this.localPause(); + // check if looping is on, if so set time back to 0 and restart + if (this.state.roomLoop) { + this.localSeek(0); + this.localPlay(); + return; + } + if (this.state.playlist.length) { + // Pass the url of the video at the time this video was started + this.socket.emit("CMD:playlistNext", url); + return; + } + // Play next fileIndex + const re = /&fileIndex=(\d+)$/; + const match = re.exec(this.state.roomMedia); + if (match) { + const fileIndex = match[1]; + const nextNum = Number(fileIndex) + 1; + const nextUrl = this.state.roomMedia.replace( + /&fileIndex=(\d+)$/, + `&fileIndex=${nextNum}`, + ); + this.roomSetMedia(nextUrl); + } + }; + + refreshControls = () => { + this.setState({ controlsTimestamp: Date.now() }); + }; + + setSettingsModalOpen = (settingsModalOpen: boolean) => { + this.setState({ settingsModalOpen }); + }; + + render() { + const sharer = this.state.participants.find((p) => p.isScreenShare); + const playlist = this.state.playlist; + const controls = ( + + ); + return ( + + {this.state.isMultiSelectModalOpen && ( + + )} + {this.state.isVBrowserModalOpen && ( + this.setState({ isVBrowserModalOpen: false })} + startVBrowser={this.startVBrowser} + /> + )} + {this.state.isScreenShareModalOpen && ( + this.setState({ isScreenShareModalOpen: false })} + startScreenShare={this.startScreenShare} + /> + )} + {this.state.isFileShareModalOpen && ( + this.setState({ isFileShareModalOpen: false })} + startFileShare={this.startFileShare} + startConvert={this.startConvert} + /> + )} + {this.state.isSubtitleModalOpen && ( + this.setState({ isSubtitleModalOpen: false })} + socket={this.socket} + roomSubtitle={this.state.roomSubtitle} + roomMedia={this.state.roomMedia} + haveLock={this.haveLock} + getMediaDisplayName={this.getMediaDisplayName} + setSubtitleMode={this.Player().setSubtitleMode} + getSubtitleMode={this.Player().getSubtitleMode} + /> + )} + {this.state.state === "starting" && ( + + Loading... + + )} + {this.state.overlayMsg && } + {this.state.isErrorAuth && } + + {this.state.errorMessage && ( + + {this.state.errorMessage} + + )} + {this.state.successMessage && ( + + {this.state.successMessage} + + )} + {this.state.warningMessage && ( + + {this.state.warningMessage} + + )} + {!this.state.fullScreen && ( + + )} + { +
+
+
+ {!this.state.fullScreen && ( + +
+ +
+
+ {this.localStreamToPublish && ( + + )} + {!this.localStreamToPublish && + !sharer && + !this.playingVBrowser() && ( + + )} + {!this.localStreamToPublish && + !sharer && + !this.playingVBrowser() && ( + + )} + {this.playingVBrowser() && ( + <> + + + + + + )} + {!this.localStreamToPublish && + !sharer && + !this.playingVBrowser() && ( + + )} + {this.state.uploadController && ( + + )} + {false && ( + + )} + {Boolean(this.context.streamPath) && ( + + )} + + + + + 0 ? "scroll" : undefined, + maxHeight: 400, + maxWidth: isMobile() ? 400 : 600, + }} + > + {playlist.length === 0 && ( + + There are no items in the playlist. + + )} + {playlist.map( + (item: PlaylistVideo, index: number) => { + if (Boolean(item.img)) { + item.type = "youtube"; + } + return ( + + { + this.roomPlaylistMove(index, 0); + }} + onRemove={(index) => { + this.roomPlaylistDelete(index); + }} + disabled={!this.haveLock()} + /> + + ); + }, + )} + + +
+
+ )} +
+
+ {!this.state.isAutoPlayable && this.state.roomMedia && ( + + + + )} + {(this.state.loading || + !this.state.roomMedia || + this.state.nonPlayableMedia) && + !this.state.isLiveStream && ( +
+ {this.state.loading && ( +
+ +
+ {this.playingVBrowser() + ? "Launching virtual browser. This can take up to a minute." + : ""} +
+
+ )} + {!this.state.loading && !this.state.roomMedia && ( + + Pick something to watch above. + + )} + {!this.state.loading && + this.state.nonPlayableMedia && ( + + Maybe you meant to launch a VBrowser if you're + trying to visit a web page? + + )} +
+ )} +