From 4348f76a7cf519a4e3e4d8fc507ff93936c60136 Mon Sep 17 00:00:00 2001 From: wisecolt Date: Fri, 2 Jan 2026 15:49:01 +0300 Subject: [PATCH] first commit --- .env.example | 17 + .gitignore | 82 + Dockerfile | 24 + README.md | 59 + apps/server/Dockerfile | 10 + apps/server/package.json | 38 + apps/server/src/auth/auth.middleware.ts | 22 + apps/server/src/auth/auth.routes.ts | 64 + apps/server/src/auth/auth.service.ts | 48 + apps/server/src/config.ts | 32 + .../src/enforcement/enforcement.types.ts | 5 + .../src/enforcement/enforcement.worker.ts | 122 + apps/server/src/index.ts | 112 + apps/server/src/loop/loop.engine.ts | 219 + apps/server/src/loop/loop.routes.ts | 151 + apps/server/src/loop/loop.scheduler.ts | 26 + apps/server/src/loop/loop.types.ts | 12 + apps/server/src/loop/profiles.routes.ts | 74 + apps/server/src/qbit/qbit.capabilities.ts | 30 + apps/server/src/qbit/qbit.client.ts | 177 + apps/server/src/qbit/qbit.context.ts | 22 + apps/server/src/qbit/qbit.routes.ts | 30 + apps/server/src/qbit/qbit.types.ts | 51 + apps/server/src/realtime/emitter.ts | 43 + apps/server/src/realtime/events.ts | 9 + apps/server/src/realtime/socket.ts | 59 + apps/server/src/status/status.routes.ts | 11 + apps/server/src/status/status.service.ts | 45 + apps/server/src/storage/jsondb.ts | 90 + apps/server/src/storage/mutex.ts | 12 + apps/server/src/storage/paths.ts | 7 + apps/server/src/timer/timer.routes.ts | 58 + apps/server/src/timer/timer.types.ts | 4 + apps/server/src/timer/timer.worker.ts | 94 + apps/server/src/torrent/torrent.archive.ts | 46 + apps/server/src/torrent/torrent.generator.ts | 41 + apps/server/src/torrent/torrent.routes.ts | 121 + apps/server/src/types.ts | 131 + apps/server/src/utils/logger.ts | 34 + apps/server/src/utils/time.ts | 7 + apps/server/src/utils/validators.ts | 25 + apps/server/tsconfig.json | 14 + apps/web/Dockerfile | 10 + apps/web/index.html | 19 + apps/web/package.json | 33 + apps/web/postcss.config.cjs | 6 + apps/web/src/App.tsx | 33 + apps/web/src/api/client.ts | 8 + apps/web/src/components/layout/AppLayout.tsx | 137 + apps/web/src/components/layout/Shell.tsx | 9 + .../components/loop/AdvancedUploadCard.tsx | 111 + apps/web/src/components/loop/LogsPanel.tsx | 65 + .../web/src/components/loop/LoopSetupCard.tsx | 111 + .../web/src/components/loop/LoopStatsCard.tsx | 92 + apps/web/src/components/loop/ProfilesCard.tsx | 185 + .../torrents/TorrentDetailsCard.tsx | 111 + .../src/components/torrents/TorrentTable.tsx | 219 + apps/web/src/components/ui/Alert.tsx | 36 + apps/web/src/components/ui/AlertDialog.tsx | 69 + .../web/src/components/ui/AlertToastStack.tsx | 24 + apps/web/src/components/ui/Badge.tsx | 27 + apps/web/src/components/ui/Button.tsx | 21 + apps/web/src/components/ui/Card.tsx | 24 + apps/web/src/components/ui/Input.tsx | 12 + apps/web/src/index.css | 193 + apps/web/src/main.tsx | 10 + apps/web/src/pages/DashboardPage.tsx | 40 + apps/web/src/pages/LoginPage.tsx | 47 + apps/web/src/pages/TimerPage.tsx | 530 ++ apps/web/src/socket/socket.ts | 67 + apps/web/src/store/useAppStore.ts | 140 + apps/web/src/store/useAuthStore.ts | 40 + apps/web/src/store/useUiStore.ts | 31 + apps/web/tailwind.config.cjs | 14 + apps/web/tsconfig.json | 13 + apps/web/vite.config.ts | 15 + docker-compose.yml | 22 + package.json | 13 + pnpm-lock.yaml | 5316 +++++++++++++++++ pnpm-workspace.yaml | 2 + 80 files changed, 10133 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 apps/server/Dockerfile create mode 100644 apps/server/package.json create mode 100644 apps/server/src/auth/auth.middleware.ts create mode 100644 apps/server/src/auth/auth.routes.ts create mode 100644 apps/server/src/auth/auth.service.ts create mode 100644 apps/server/src/config.ts create mode 100644 apps/server/src/enforcement/enforcement.types.ts create mode 100644 apps/server/src/enforcement/enforcement.worker.ts create mode 100644 apps/server/src/index.ts create mode 100644 apps/server/src/loop/loop.engine.ts create mode 100644 apps/server/src/loop/loop.routes.ts create mode 100644 apps/server/src/loop/loop.scheduler.ts create mode 100644 apps/server/src/loop/loop.types.ts create mode 100644 apps/server/src/loop/profiles.routes.ts create mode 100644 apps/server/src/qbit/qbit.capabilities.ts create mode 100644 apps/server/src/qbit/qbit.client.ts create mode 100644 apps/server/src/qbit/qbit.context.ts create mode 100644 apps/server/src/qbit/qbit.routes.ts create mode 100644 apps/server/src/qbit/qbit.types.ts create mode 100644 apps/server/src/realtime/emitter.ts create mode 100644 apps/server/src/realtime/events.ts create mode 100644 apps/server/src/realtime/socket.ts create mode 100644 apps/server/src/status/status.routes.ts create mode 100644 apps/server/src/status/status.service.ts create mode 100644 apps/server/src/storage/jsondb.ts create mode 100644 apps/server/src/storage/mutex.ts create mode 100644 apps/server/src/storage/paths.ts create mode 100644 apps/server/src/timer/timer.routes.ts create mode 100644 apps/server/src/timer/timer.types.ts create mode 100644 apps/server/src/timer/timer.worker.ts create mode 100644 apps/server/src/torrent/torrent.archive.ts create mode 100644 apps/server/src/torrent/torrent.generator.ts create mode 100644 apps/server/src/torrent/torrent.routes.ts create mode 100644 apps/server/src/types.ts create mode 100644 apps/server/src/utils/logger.ts create mode 100644 apps/server/src/utils/time.ts create mode 100644 apps/server/src/utils/validators.ts create mode 100644 apps/server/tsconfig.json create mode 100644 apps/web/Dockerfile create mode 100644 apps/web/index.html create mode 100644 apps/web/package.json create mode 100644 apps/web/postcss.config.cjs create mode 100644 apps/web/src/App.tsx create mode 100644 apps/web/src/api/client.ts create mode 100644 apps/web/src/components/layout/AppLayout.tsx create mode 100644 apps/web/src/components/layout/Shell.tsx create mode 100644 apps/web/src/components/loop/AdvancedUploadCard.tsx create mode 100644 apps/web/src/components/loop/LogsPanel.tsx create mode 100644 apps/web/src/components/loop/LoopSetupCard.tsx create mode 100644 apps/web/src/components/loop/LoopStatsCard.tsx create mode 100644 apps/web/src/components/loop/ProfilesCard.tsx create mode 100644 apps/web/src/components/torrents/TorrentDetailsCard.tsx create mode 100644 apps/web/src/components/torrents/TorrentTable.tsx create mode 100644 apps/web/src/components/ui/Alert.tsx create mode 100644 apps/web/src/components/ui/AlertDialog.tsx create mode 100644 apps/web/src/components/ui/AlertToastStack.tsx create mode 100644 apps/web/src/components/ui/Badge.tsx create mode 100644 apps/web/src/components/ui/Button.tsx create mode 100644 apps/web/src/components/ui/Card.tsx create mode 100644 apps/web/src/components/ui/Input.tsx create mode 100644 apps/web/src/index.css create mode 100644 apps/web/src/main.tsx create mode 100644 apps/web/src/pages/DashboardPage.tsx create mode 100644 apps/web/src/pages/LoginPage.tsx create mode 100644 apps/web/src/pages/TimerPage.tsx create mode 100644 apps/web/src/socket/socket.ts create mode 100644 apps/web/src/store/useAppStore.ts create mode 100644 apps/web/src/store/useAuthStore.ts create mode 100644 apps/web/src/store/useUiStore.ts create mode 100644 apps/web/tailwind.config.cjs create mode 100644 apps/web/tsconfig.json create mode 100644 apps/web/vite.config.ts create mode 100644 docker-compose.yml create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b95265f --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +QBIT_BASE_URL=http://zimaos.bee:8181 +QBIT_USERNAME=admin +QBIT_PASSWORD=adminadmin +APP_USERNAME=qbuffer +APP_PASSWORD=changeme +JWT_SECRET=replace_me +APP_HOST=localhost +WEB_ORIGIN=http://localhost:5173 +SERVER_PORT=3001 +WEB_PORT=5173 +POLL_INTERVAL_MS=3000 +ENFORCE_INTERVAL_MS=2000 +DEFAULT_DELAY_MS=3000 +MAX_LOOP_LIMIT=1000 +STALLED_RECOVERY_MS=300000 +TIMER_POLL_MS=60000 +NODE_ENV=development diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e375eec --- /dev/null +++ b/.gitignore @@ -0,0 +1,82 @@ +# Node modules +*/node_modules/ +jspm_packages/ +/data +.kilocode/ +client/dist +claudedocs/ +data/ +node_modules/ +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Runtime data +pids *.pid *.seed *.pid.lock + +# Coverage directory +.nyc_output/ +coverage/ +lcov-report/ + +# Environment variables +.env +.env.*.local +!.env.example +.pnpm-store/ + +# Build / output directories +/dist/ +/build/ +/output/ +/. svelte-kit/ +.svelte-kit/ +.vite/ +.tmp/ +.cache/ + +# Docker files / volumes +docker-compose.override.yml +docker-compose.*.yml +docker/*-volume/ +docker/*-data/ +*.tar +*.img + +# OS / IDE stuff +.DS_Store +Thumbs.db +desktop.ini +*.swp +.vscode/ +.idea/ +*.sublime-project +*.sublime-workspace + +# Media / Download directories (depending on your setup) +downloads/ +cache/ +movie/movieData/ +movie/movieData/**/subtitles/ +movie/movieData/**/poster.jpg +movie/movieData/**/backdrop.jpg + +# Generic placeholders +*.gitkeep + +# Torrent / upload temp files +/uploads/ +/uploads/* +*.torrent +*.part +*.temp + +# Other sensitive files +/key.pem +/cert.pem +*.log + +# Client +client/src/assets/avatar_.png \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..518b51e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM node:20-alpine AS base +WORKDIR /app +RUN corepack enable + +FROM base AS deps +COPY package.json pnpm-workspace.yaml ./ +COPY apps/server/package.json apps/server/package.json +COPY apps/web/package.json apps/web/package.json +RUN pnpm install --frozen-lockfile=false + +FROM deps AS build +COPY . . +RUN pnpm -C apps/web build +RUN pnpm -C apps/server build + +FROM base AS prod +ENV NODE_ENV=production +WORKDIR /app +COPY --from=deps /app/node_modules /app/node_modules +COPY --from=build /app/apps/server/dist /app/apps/server/dist +COPY --from=build /app/apps/server/package.json /app/apps/server/package.json +COPY --from=build /app/apps/server/public /app/apps/server/public +EXPOSE 3001 +CMD ["node", "/app/apps/server/dist/index.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..6aed266 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# q-buffer + +A production-ready monorepo that orchestrates qBittorrent torrents in a controlled playback loop with strict peer enforcement. + +## Overview + +- Backend: Node.js + TypeScript + Express + socket.io +- Frontend: React + Vite + TypeScript (shadcn-style UI) +- Storage: JSON file DB with atomic writes and mutex +- Docker: dev (two containers) and prod (single container) + +## Quick Start + +1. Copy `.env.example` to `.env` and fill values. +2. Start dev stack: + +```bash +docker-compose -f docker-compose.dev.yml up --build +``` + +3. Open: + +- Web: http://localhost:5173 +- API/Socket: http://localhost:3001 + +## Production + +```bash +docker-compose up --build +``` + +Open http://localhost:3001 + +## Features + +- Login with env-configured credentials (JWT httpOnly cookie) +- Torrent list and selection +- Torrent archive to `/data/torrents/{hash}.torrent` +- Loop engine with delete/re-add between runs +- Aggressive allow-IP enforcement (peer ban when supported) +- Dry run report and profiles +- Real-time status/logs via socket.io + +## Environment Variables + +- `QBIT_BASE_URL`, `QBIT_USERNAME`, `QBIT_PASSWORD` +- `APP_USERNAME`, `APP_PASSWORD`, `JWT_SECRET` +- `POLL_INTERVAL_MS`, `ENFORCE_INTERVAL_MS`, `DEFAULT_DELAY_MS`, `MAX_LOOP_LIMIT` + +## Folder Layout + +- `apps/server`: Express API + socket.io +- `apps/web`: Vite React UI +- `data`: JSON DB, logs, torrent archive + +## Notes + +- If magnet metadata generation fails, use Advanced upload to provide `.torrent` manually. +- The loop engine deletes downloaded data between loops. diff --git a/apps/server/Dockerfile b/apps/server/Dockerfile new file mode 100644 index 0000000..d2af8f6 --- /dev/null +++ b/apps/server/Dockerfile @@ -0,0 +1,10 @@ +FROM node:20-alpine +WORKDIR /app +RUN corepack enable +COPY package.json /app/apps/server/package.json +COPY package.json /app/package.json +COPY pnpm-workspace.yaml /app/pnpm-workspace.yaml +RUN pnpm install --frozen-lockfile=false +WORKDIR /app/apps/server +EXPOSE 3001 +CMD ["pnpm", "dev"] diff --git a/apps/server/package.json b/apps/server/package.json new file mode 100644 index 0000000..67e1563 --- /dev/null +++ b/apps/server/package.json @@ -0,0 +1,38 @@ +{ + "name": "q-buffer-server", + "private": true, + "scripts": { + "dev": "ts-node-dev --respawn --transpile-only src/index.ts", + "build": "tsc -p tsconfig.json", + "start": "node dist/index.js" + }, + "dependencies": { + "axios": "^1.7.7", + "axios-cookiejar-support": "^5.0.2", + "bcryptjs": "^2.4.3", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", + "express": "^4.19.2", + "express-rate-limit": "^7.3.1", + "form-data": "^4.0.0", + "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1", + "parse-torrent": "^9.1.5", + "pino": "^9.3.2", + "socket.io": "^4.7.5", + "tough-cookie": "^4.1.4", + "webtorrent": "^2.4.5", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/cookie-parser": "^1.4.7", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.6", + "@types/multer": "^1.4.12", + "@types/node": "^20.14.9", + "ts-node-dev": "^2.0.0", + "typescript": "^5.5.3" + } +} diff --git a/apps/server/src/auth/auth.middleware.ts b/apps/server/src/auth/auth.middleware.ts new file mode 100644 index 0000000..62f8231 --- /dev/null +++ b/apps/server/src/auth/auth.middleware.ts @@ -0,0 +1,22 @@ +import { Request, Response, NextFunction } from "express"; +import { verifyToken } from "./auth.service" + +export const requireAuth = (req: Request, res: Response, next: NextFunction) => { + const token = req.cookies?.["qbuffer_token"]; + if (!token) { + return res.status(401).json({ error: "Unauthorized" }); + } + try { + const payload = verifyToken(token); + req.user = payload; + return next(); + } catch (error) { + return res.status(401).json({ error: "Unauthorized" }); + } +}; + +declare module "express-serve-static-core" { + interface Request { + user?: { username: string }; + } +} diff --git a/apps/server/src/auth/auth.routes.ts b/apps/server/src/auth/auth.routes.ts new file mode 100644 index 0000000..f0355a3 --- /dev/null +++ b/apps/server/src/auth/auth.routes.ts @@ -0,0 +1,64 @@ +import { Router } from "express"; +import rateLimit from "express-rate-limit"; +import { signToken, verifyCredentials, verifyToken } from "./auth.service" +import { isDev } from "../config" + +const router = Router(); + +const loginLimiter = rateLimit({ + windowMs: 60_000, + max: 5, + standardHeaders: true, + legacyHeaders: false, +}); + +router.post("/login", loginLimiter, async (req, res) => { + const { username, password } = req.body ?? {}; + if (!username || !password) { + return res.status(400).json({ error: "Missing credentials" }); + } + const user = await verifyCredentials(username, password); + if (!user) { + return res.status(401).json({ error: "Invalid credentials" }); + } + const token = signToken({ username: user.username }); + res.cookie("qbuffer_token", token, { + httpOnly: true, + sameSite: "lax", + secure: !isDev, + }); + return res.json({ username: user.username }); +}); + +router.post("/logout", (_req, res) => { + res.clearCookie("qbuffer_token"); + return res.json({ ok: true }); +}); + +router.get("/me", (req, res) => { + const token = req.cookies?.["qbuffer_token"]; + if (!token) { + return res.status(401).json({ error: "Unauthorized" }); + } + try { + const payload = verifyToken(token); + return res.json({ ok: true, username: payload.username }); + } catch (error) { + return res.status(401).json({ error: "Unauthorized" }); + } +}); + +router.get("/socket-token", (req, res) => { + const token = req.cookies?.["qbuffer_token"]; + if (!token) { + return res.status(401).json({ error: "Unauthorized" }); + } + try { + verifyToken(token); + return res.json({ token }); + } catch (error) { + return res.status(401).json({ error: "Unauthorized" }); + } +}); + +export default router; diff --git a/apps/server/src/auth/auth.service.ts b/apps/server/src/auth/auth.service.ts new file mode 100644 index 0000000..15bcaf3 --- /dev/null +++ b/apps/server/src/auth/auth.service.ts @@ -0,0 +1,48 @@ +import bcrypt from "bcryptjs"; +import jwt from "jsonwebtoken"; +import { config } from "../config" +import { readDb, writeDb } from "../storage/jsondb" +import { nowIso } from "../utils/time" +import { User } from "../types" + +const ensureSeedUser = async (): Promise => { + if (!config.appUsername || !config.appPassword) { + return null; + } + const db = await readDb(); + const existing = db.users.find((user) => user.username === config.appUsername); + if (existing) { + return existing; + } + const passwordHash = await bcrypt.hash(config.appPassword, 10); + const newUser: User = { + username: config.appUsername, + passwordHash, + createdAt: nowIso(), + }; + db.users.push(newUser); + await writeDb(db); + return newUser; +}; + +export const initAuth = async () => { + await ensureSeedUser(); +}; + +export const verifyCredentials = async (username: string, password: string) => { + const db = await readDb(); + const user = db.users.find((u) => u.username === username); + if (!user) { + return null; + } + const match = await bcrypt.compare(password, user.passwordHash); + return match ? user : null; +}; + +export const signToken = (payload: { username: string }) => { + return jwt.sign(payload, config.jwtSecret, { expiresIn: "7d" }); +}; + +export const verifyToken = (token: string) => { + return jwt.verify(token, config.jwtSecret) as { username: string }; +}; diff --git a/apps/server/src/config.ts b/apps/server/src/config.ts new file mode 100644 index 0000000..68a6540 --- /dev/null +++ b/apps/server/src/config.ts @@ -0,0 +1,32 @@ +import path from "node:path"; + +const envNumber = (value: string | undefined, fallback: number) => { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : fallback; +}; + +export const config = { + port: envNumber(process.env.SERVER_PORT, 3001), + nodeEnv: process.env.NODE_ENV ?? "development", + qbitBaseUrl: process.env.QBIT_BASE_URL ?? "", + qbitUsername: process.env.QBIT_USERNAME ?? "", + qbitPassword: process.env.QBIT_PASSWORD ?? "", + appUsername: process.env.APP_USERNAME ?? "", + appPassword: process.env.APP_PASSWORD ?? "", + jwtSecret: process.env.JWT_SECRET ?? "", + pollIntervalMs: envNumber(process.env.POLL_INTERVAL_MS, 3000), + enforceIntervalMs: envNumber(process.env.ENFORCE_INTERVAL_MS, 2000), + defaultDelayMs: envNumber(process.env.DEFAULT_DELAY_MS, 3000), + maxLoopLimit: envNumber(process.env.MAX_LOOP_LIMIT, 20), + stalledRecoveryMs: envNumber(process.env.STALLED_RECOVERY_MS, 300_000), + timerPollMs: envNumber(process.env.TIMER_POLL_MS, 60_000), + webPort: envNumber(process.env.WEB_PORT, 5173), + webOrigin: process.env.WEB_ORIGIN ?? "", + dataDir: "/app/data", + dbPath: "/app/data/db.json", + logsPath: "/app/data/logs.json", + torrentArchiveDir: "/app/data/torrents", + webPublicDir: path.resolve("/app/apps/server/public"), +}; + +export const isDev = config.nodeEnv !== "production"; diff --git a/apps/server/src/enforcement/enforcement.types.ts b/apps/server/src/enforcement/enforcement.types.ts new file mode 100644 index 0000000..67ab16e --- /dev/null +++ b/apps/server/src/enforcement/enforcement.types.ts @@ -0,0 +1,5 @@ +export interface EnforcementResult { + jobId: string; + bannedIps: string[]; + allowIpConnected: boolean; +} diff --git a/apps/server/src/enforcement/enforcement.worker.ts b/apps/server/src/enforcement/enforcement.worker.ts new file mode 100644 index 0000000..3a0269f --- /dev/null +++ b/apps/server/src/enforcement/enforcement.worker.ts @@ -0,0 +1,122 @@ +import axios from "axios"; +import { getQbitCapabilities, getQbitClient } from "../qbit/qbit.context"; +import { readDb, writeDb } from "../storage/jsondb"; +import { nowIso } from "../utils/time"; +import { emitJobLog, emitJobMetrics } from "../realtime/emitter"; +import { appendAuditLog } from "../utils/logger"; + +const peerErrorThrottle = new Map(); + +export const startEnforcementWorker = (intervalMs: number) => { + setInterval(async () => { + let db; + try { + const qbit = getQbitClient(); + const caps = getQbitCapabilities(); + db = await readDb(); + + for (const job of db.loopJobs) { + try { + if (job.status !== "RUNNING") { + continue; + } + if (!caps) { + continue; + } + if (!caps?.hasPeersEndpoint) { + emitJobLog({ + jobId: job.id, + level: "WARN", + message: "Peer listing unsupported; enforcement disabled", + createdAt: nowIso(), + }); + continue; + } + let peersResponse; + try { + peersResponse = await qbit.getTorrentPeers(job.torrentHash); + } catch (error) { + const status = axios.isAxiosError(error) ? error.response?.status : undefined; + if (status === 404) { + const lastWarn = peerErrorThrottle.get(job.id) ?? 0; + if (Date.now() - lastWarn > 60_000) { + emitJobLog({ + jobId: job.id, + level: "WARN", + message: "Peer listesi desteklenmiyor; enforcement devre dışı.", + createdAt: nowIso(), + }); + peerErrorThrottle.set(job.id, Date.now()); + } + continue; + } + throw error; + } + const peers = Object.values(peersResponse.peers || {}); + let allowIpConnected = false; + const banned: string[] = []; + + for (const peer of peers) { + if (peer.ip === job.allowIp) { + allowIpConnected = true; + continue; + } + if (caps?.hasBanEndpoint) { + const peerKey = `${peer.ip}:${peer.port}`; + banned.push(peerKey); + } + } + + if (banned.length > 0 && caps?.hasBanEndpoint) { + await qbit.banPeers(banned); + job.bans.bannedIps.push(...banned.map((peer) => peer.split(":")[0])); + job.bans.lastBanAt = nowIso(); + emitJobLog({ + jobId: job.id, + level: "WARN", + message: `Banned ${banned.length} peers`, + createdAt: nowIso(), + }); + await appendAuditLog({ + level: "WARN", + event: "PEER_BANNED", + message: `Job ${job.id}: banned ${banned.length} peers`, + }); + } + + if (!caps?.hasBanEndpoint) { + emitJobLog({ + jobId: job.id, + level: "WARN", + message: "Peer ban unsupported; warn-only enforcement", + createdAt: nowIso(), + }); + } + + if (!allowIpConnected) { + emitJobLog({ + jobId: job.id, + level: "WARN", + message: "Allowed IP not connected", + createdAt: nowIso(), + }); + } + + job.updatedAt = nowIso(); + emitJobMetrics(job); + } catch (error) { + emitJobLog({ + jobId: job.id, + level: "ERROR", + message: "Enforcement error; continuing.", + createdAt: nowIso(), + }); + } + } + + await writeDb(db); + } catch (error) { + // Keep worker alive on errors. + } + }, intervalMs); +}; diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts new file mode 100644 index 0000000..72e1d6a --- /dev/null +++ b/apps/server/src/index.ts @@ -0,0 +1,112 @@ +import express from "express"; +import http from "node:http"; +import path from "node:path"; +import cookieParser from "cookie-parser"; +import cors from "cors"; +import { config, isDev } from "./config" +import { ensureDataPaths } from "./storage/paths" +import { initAuth } from "./auth/auth.service" +import authRoutes from "./auth/auth.routes" +import { requireAuth } from "./auth/auth.middleware" +import qbitRoutes from "./qbit/qbit.routes" +import torrentRoutes from "./torrent/torrent.routes" +import loopRoutes from "./loop/loop.routes" +import profilesRoutes from "./loop/profiles.routes" +import statusRoutes from "./status/status.routes" +import timerRoutes from "./timer/timer.routes" +import { QbitClient } from "./qbit/qbit.client" +import { detectCapabilities } from "./qbit/qbit.capabilities" +import { setQbitClient, setQbitCapabilities } from "./qbit/qbit.context" +import { setQbitStatus } from "./status/status.service" +import { createSocketServer } from "./realtime/socket" +import { initEmitter, emitQbitHealth } from "./realtime/emitter" +import { startLoopScheduler } from "./loop/loop.scheduler" +import { startEnforcementWorker } from "./enforcement/enforcement.worker" +import { startTimerWorker } from "./timer/timer.worker" +import { logger } from "./utils/logger" + +process.on("unhandledRejection", (reason) => { + logger.error({ reason }, "Unhandled promise rejection"); +}); + +process.on("uncaughtException", (error) => { + logger.error({ error }, "Uncaught exception"); +}); + +let serverStarted = false; + +const bootstrap = async () => { + await ensureDataPaths(); + await initAuth(); + + const app = express(); + app.use(cookieParser()); + app.use(express.json()); + + if (isDev) { + const fallbackOrigin = `http://localhost:${config.webPort}`; + const origins = [config.webOrigin || fallbackOrigin, fallbackOrigin]; + app.use( + cors({ + origin: origins, + credentials: true, + }) + ); + } + + app.use("/api/auth", authRoutes); + app.use("/api/qbit", requireAuth, qbitRoutes); + app.use("/api/torrent", requireAuth, torrentRoutes); + app.use("/api/loop", requireAuth, loopRoutes); + app.use("/api/profiles", requireAuth, profilesRoutes); + app.use("/api/status", requireAuth, statusRoutes); + app.use("/api/timer", requireAuth, timerRoutes); + + if (!isDev) { + app.use(express.static(config.webPublicDir)); + app.get("*", (req, res, next) => { + if (req.path.startsWith("/api")) { + return next(); + } + return res.sendFile(path.join(config.webPublicDir, "index.html")); + }); + } + + const server = http.createServer(app); + const io = createSocketServer(server); + initEmitter(io); + + const qbit = new QbitClient(); + setQbitClient(qbit); + + try { + const caps = await detectCapabilities(qbit); + setQbitCapabilities(caps); + setQbitStatus({ ok: true, version: caps.version, capabilities: caps }); + emitQbitHealth({ ok: true, version: caps.version, capabilities: caps }); + } catch (error) { + logger.error({ error }, "Failed to connect to qBittorrent"); + setQbitStatus({ ok: false, lastError: (error as Error).message }); + emitQbitHealth({ ok: false, lastError: (error as Error).message }); + } + + startLoopScheduler(qbit, config.pollIntervalMs); + startEnforcementWorker(config.enforceIntervalMs); + startTimerWorker(qbit, config.timerPollMs); + + server.listen(config.port, () => { + serverStarted = true; + logger.info(`q-buffer server listening on ${config.port}`); + }); +}; + +const startWithRetry = () => { + bootstrap().catch((error) => { + logger.error({ error }, "Failed to start server"); + if (!serverStarted) { + setTimeout(startWithRetry, 5000); + } + }); +}; + +startWithRetry(); diff --git a/apps/server/src/loop/loop.engine.ts b/apps/server/src/loop/loop.engine.ts new file mode 100644 index 0000000..ab6874e --- /dev/null +++ b/apps/server/src/loop/loop.engine.ts @@ -0,0 +1,219 @@ +import { randomUUID } from "node:crypto"; +import { QbitClient } from "../qbit/qbit.client"; +import { readDb, writeDb } from "../storage/jsondb"; +import { LoopJob } from "../types"; +import { nowIso } from "../utils/time"; +import { appendAuditLog, logger } from "../utils/logger"; +import { emitJobLog, emitJobMetrics } from "../realtime/emitter"; +import { config } from "../config"; + +const logJob = async (jobId: string, level: "INFO" | "WARN" | "ERROR", message: string, event?: string) => { + const createdAt = nowIso(); + emitJobLog({ jobId, level, message, createdAt }); + if (event) { + await appendAuditLog({ level, event: event as any, message }); + } +}; + +export const createLoopJob = async ( + input: { + torrentHash: string; + name: string; + sizeBytes: number; + magnet?: string; + torrentFilePath?: string; + allowIp: string; + targetLoops: number; + delayMs: number; + } +): Promise => { + const now = nowIso(); + const job: LoopJob = { + id: randomUUID(), + torrentHash: input.torrentHash, + name: input.name, + sizeBytes: input.sizeBytes, + magnet: input.magnet, + torrentFilePath: input.torrentFilePath, + allowIp: input.allowIp, + targetLoops: input.targetLoops, + doneLoops: 0, + delayMs: input.delayMs, + deleteDataBetweenLoops: true, + enforcementMode: "aggressive-soft", + status: "RUNNING", + currentRun: { + startedAt: now, + lastProgress: 0, + lastProgressAt: now, + downloadedThisRunBytes: 0, + avgSpeed: 0, + }, + totals: { + totalDownloadedBytes: 0, + totalTimeMs: 0, + }, + bans: { + bannedIps: [], + }, + createdAt: now, + updatedAt: now, + }; + const db = await readDb(); + db.loopJobs.push(job); + await writeDb(db); + await logJob(job.id, "INFO", `Loop job started for ${job.name}`, "JOB_STARTED"); + return job; +}; + +export const stopLoopJob = async (jobId: string) => { + const db = await readDb(); + const job = db.loopJobs.find((j) => j.id === jobId); + if (!job) { + return null; + } + job.status = "STOPPED"; + job.nextRunAt = undefined; + job.currentRun = undefined; + job.updatedAt = nowIso(); + await writeDb(db); + await logJob(job.id, "WARN", "Loop job stopped by user"); + return job; +}; + +export const updateJob = async (job: LoopJob) => { + const db = await readDb(); + const index = db.loopJobs.findIndex((j) => j.id === job.id); + if (index === -1) { + return null; + } + db.loopJobs[index] = job; + await writeDb(db); + emitJobMetrics(job); + return job; +}; + +export const tickLoopJobs = async ( + qbit: QbitClient, + torrents: { hash: string; progress: number; state: string; dlspeed: number }[] +) => { + const db = await readDb(); + let changed = false; + + for (const job of db.loopJobs) { + if (job.status === "RUNNING") { + const torrent = torrents.find((t) => t.hash === job.torrentHash); + if (!torrent) { + try { + if (job.torrentFilePath) { + await qbit.addTorrentByFile(job.torrentFilePath); + } else if (job.magnet) { + await qbit.addTorrentByMagnet(job.magnet); + } + await logJob(job.id, "WARN", "Torrent missing, re-added", "JOB_RESTARTED"); + } catch (error) { + job.status = "ERROR"; + job.lastError = "Failed to re-add torrent"; + await logJob(job.id, "ERROR", job.lastError); + } + job.updatedAt = nowIso(); + changed = true; + continue; + } + + job.currentRun = job.currentRun ?? { + startedAt: nowIso(), + lastProgress: 0, + lastProgressAt: nowIso(), + downloadedThisRunBytes: 0, + avgSpeed: 0, + }; + if (torrent.progress > job.currentRun.lastProgress) { + job.currentRun.lastProgress = torrent.progress; + job.currentRun.lastProgressAt = nowIso(); + job.currentRun.stalledSince = undefined; + } + job.currentRun.avgSpeed = torrent.dlspeed; + job.updatedAt = nowIso(); + + const stalledState = /stalled|meta/i.test(torrent.state); + if (stalledState) { + if (!job.currentRun.stalledSince) { + job.currentRun.stalledSince = nowIso(); + } + const lastProgressAt = job.currentRun.lastProgressAt + ? new Date(job.currentRun.lastProgressAt).getTime() + : 0; + if (Date.now() - lastProgressAt > config.stalledRecoveryMs) { + await logJob( + job.id, + "WARN", + "Stalled recovery: torrent will be removed and re-added" + ); + try { + await qbit.deleteTorrent(job.torrentHash, true); + } catch (error) { + logger.error({ error }, "Failed to delete stalled torrent"); + } + job.status = "WAITING_DELAY"; + job.nextRunAt = new Date(Date.now() + job.delayMs).toISOString(); + job.updatedAt = nowIso(); + changed = true; + continue; + } + } + + if (torrent.progress >= 1 && /UP|uploading|stalledUP|pausedUP|queuedUP/i.test(torrent.state)) { + job.doneLoops += 1; + job.totals.totalDownloadedBytes += job.sizeBytes; + await logJob(job.id, "INFO", `Loop ${job.doneLoops} completed`, "JOB_COMPLETED_LOOP"); + try { + await qbit.deleteTorrent(job.torrentHash, true); + } catch (error) { + logger.error({ error }, "Failed to delete torrent"); + } + if (job.doneLoops >= job.targetLoops) { + job.status = "COMPLETED"; + await logJob(job.id, "INFO", "All loops completed", "JOB_COMPLETED_ALL"); + } else { + job.status = "WAITING_DELAY"; + job.nextRunAt = new Date(Date.now() + job.delayMs).toISOString(); + } + } + changed = true; + } + + if (job.status === "WAITING_DELAY") { + const nextRunAt = job.nextRunAt ? new Date(job.nextRunAt).getTime() : 0; + if (Date.now() >= nextRunAt) { + try { + if (job.torrentFilePath) { + await qbit.addTorrentByFile(job.torrentFilePath); + } else if (job.magnet) { + await qbit.addTorrentByMagnet(job.magnet); + } + job.status = "RUNNING"; + job.currentRun = { + startedAt: nowIso(), + lastProgress: 0, + lastProgressAt: nowIso(), + downloadedThisRunBytes: 0, + avgSpeed: 0, + }; + await logJob(job.id, "INFO", "Loop restarted", "JOB_RESTARTED"); + } catch (error) { + job.status = "ERROR"; + job.lastError = "Failed to re-add torrent after delay"; + await logJob(job.id, "ERROR", job.lastError); + } + job.updatedAt = nowIso(); + changed = true; + } + } + } + + if (changed) { + await writeDb(db); + db.loopJobs.forEach((job) => emitJobMetrics(job)); + } +}; diff --git a/apps/server/src/loop/loop.routes.ts b/apps/server/src/loop/loop.routes.ts new file mode 100644 index 0000000..1b87006 --- /dev/null +++ b/apps/server/src/loop/loop.routes.ts @@ -0,0 +1,151 @@ +import { Router } from "express"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { getQbitCapabilities, getQbitClient } from "../qbit/qbit.context"; +import { readDb } from "../storage/jsondb"; +import { createLoopJob, stopLoopJob } from "./loop.engine"; +import { dryRunSchema, loopStartSchema } from "../utils/validators"; +import { getArchiveStatus } from "../torrent/torrent.archive"; +import { config } from "../config"; +import { setArchiveStatus } from "../torrent/torrent.archive"; +import { nowIso } from "../utils/time"; + +const router = Router(); + +router.post("/start", async (req, res) => { + const parsed = loopStartSchema.safeParse(req.body); + if (!parsed.success) { + return res.status(400).json({ error: parsed.error.flatten() }); + } + const { hash, allowIp, targetLoops, delayMs } = parsed.data; + const db = await readDb(); + if (targetLoops > db.settings.maxLoopLimit) { + return res.status(400).json({ error: "Target loops exceed max limit" }); + } + const qbit = getQbitClient(); + const torrents = await qbit.getTorrentsInfo(); + const torrent = torrents.find((t) => t.hash === hash); + if (!torrent) { + return res.status(404).json({ error: "Torrent not found" }); + } + let archive = await getArchiveStatus(hash); + if (!archive?.torrentFilePath) { + try { + const buffer = await qbit.exportTorrent(hash); + const targetPath = path.join(config.torrentArchiveDir, `${hash}.torrent`); + await fs.writeFile(targetPath, buffer); + archive = await setArchiveStatus({ + hash, + status: "READY", + torrentFilePath: targetPath, + source: "exported", + updatedAt: nowIso(), + }); + } catch (error) { + return res.status(400).json({ + error: "Arşiv yok ve export başarısız. Lütfen Advanced bölümünden .torrent yükleyin.", + }); + } + } + try { + await fs.access(archive.torrentFilePath); + } catch (error) { + return res.status(400).json({ + error: "Arşiv dosyası bulunamadı. Lütfen tekrar yükleyin.", + }); + } + const job = await createLoopJob({ + torrentHash: hash, + name: torrent.name, + sizeBytes: torrent.size, + magnet: undefined, + torrentFilePath: archive?.torrentFilePath, + allowIp, + targetLoops, + delayMs, + }); + res.json(job); +}); + +router.post("/stop/:jobId", async (req, res) => { + const { jobId } = req.params; + const job = await stopLoopJob(jobId); + if (!job) { + return res.status(404).json({ error: "Job not found" }); + } + try { + const qbit = getQbitClient(); + await qbit.deleteTorrent(job.torrentHash, true); + } catch (error) { + // Best-effort delete + } + res.json(job); +}); + +router.post("/stop-by-hash", async (req, res) => { + const { hash } = req.body ?? {}; + if (!hash) { + return res.status(400).json({ error: "Missing hash" }); + } + const db = await readDb(); + const job = db.loopJobs.find((j) => j.torrentHash === hash); + if (!job) { + return res.status(404).json({ error: "Job not found" }); + } + const stopped = await stopLoopJob(job.id); + try { + const qbit = getQbitClient(); + await qbit.deleteTorrent(hash, true); + } catch (error) { + // Best-effort delete + } + res.json(stopped); +}); + +router.post("/dry-run", async (req, res) => { + const parsed = dryRunSchema.safeParse(req.body); + if (!parsed.success) { + return res.status(400).json({ error: parsed.error.flatten() }); + } + const { hash } = parsed.data; + const qbit = getQbitClient(); + const caps = getQbitCapabilities(); + const torrents = await qbit.getTorrentsInfo(); + const torrent = torrents.find((t) => t.hash === hash); + if (!torrent) { + return res.status(404).json({ error: "Torrent not found" }); + } + const archive = await getArchiveStatus(hash); + res.json({ + ok: true, + qbitVersion: caps?.version, + capabilities: caps, + hasMagnet: Boolean(torrent.magnet_uri), + archiveStatus: archive?.status ?? "MISSING", + warnings: [ + caps?.hasPeersEndpoint ? null : "Peer listing unsupported", + caps?.hasBanEndpoint ? null : "Peer ban unsupported; warn-only enforcement", + torrent.magnet_uri ? null : "Magnet unavailable; upload .torrent recommended", + ].filter(Boolean), + }); +}); + +router.get("/jobs", async (_req, res) => { + const db = await readDb(); + res.json(db.loopJobs); +}); + +router.get("/job/:jobId", async (req, res) => { + const db = await readDb(); + const job = db.loopJobs.find((j) => j.id === req.params.jobId); + if (!job) { + return res.status(404).json({ error: "Job not found" }); + } + res.json(job); +}); + +router.get("/logs/:jobId", async (req, res) => { + res.json({ jobId: req.params.jobId, logs: [] }); +}); + +export default router; diff --git a/apps/server/src/loop/loop.scheduler.ts b/apps/server/src/loop/loop.scheduler.ts new file mode 100644 index 0000000..0e2a62d --- /dev/null +++ b/apps/server/src/loop/loop.scheduler.ts @@ -0,0 +1,26 @@ +import { QbitClient } from "../qbit/qbit.client" +import { tickLoopJobs } from "./loop.engine" +import { getStatusSnapshot, refreshJobsStatus, setTorrentsStatus } from "../status/status.service" +import { emitStatusUpdate } from "../realtime/emitter" +import { logger } from "../utils/logger" + +export const startLoopScheduler = (qbit: QbitClient, intervalMs: number) => { + setInterval(async () => { + try { + const torrents = await qbit.getTorrentsInfo(); + const transfer = await qbit.getTransferInfo(); + setTorrentsStatus(torrents, transfer); + await tickLoopJobs(qbit, torrents); + const jobs = await refreshJobsStatus(); + const current = await getStatusSnapshot(); + emitStatusUpdate({ + qbit: { ...current.qbit, ok: true }, + torrents, + transfer, + jobs, + }); + } catch (error) { + logger.error({ error }, "Loop scheduler tick failed"); + } + }, intervalMs); +}; diff --git a/apps/server/src/loop/loop.types.ts b/apps/server/src/loop/loop.types.ts new file mode 100644 index 0000000..16b988d --- /dev/null +++ b/apps/server/src/loop/loop.types.ts @@ -0,0 +1,12 @@ +import { LoopJob } from "../types" + +export interface LoopStartInput { + hash: string; + allowIp: string; + targetLoops: number; + delayMs: number; +} + +export interface LoopEngineContext { + jobs: LoopJob[]; +} diff --git a/apps/server/src/loop/profiles.routes.ts b/apps/server/src/loop/profiles.routes.ts new file mode 100644 index 0000000..6848c35 --- /dev/null +++ b/apps/server/src/loop/profiles.routes.ts @@ -0,0 +1,74 @@ +import { Router } from "express"; +import { randomUUID } from "node:crypto"; +import { readDb, writeDb } from "../storage/jsondb" +import { profileSchema } from "../utils/validators" +import { nowIso } from "../utils/time" + +const router = Router(); + +router.get("/", async (_req, res) => { + const db = await readDb(); + res.json(db.profiles); +}); + +router.post("/", async (req, res) => { + const parsed = profileSchema.safeParse(req.body); + if (!parsed.success) { + return res.status(400).json({ error: parsed.error.flatten() }); + } + const db = await readDb(); + const profile = { + id: randomUUID(), + createdAt: nowIso(), + ...parsed.data, + }; + db.profiles.push(profile); + await writeDb(db); + res.json(profile); +}); + +router.put("/:profileId", async (req, res) => { + const parsed = profileSchema.safeParse(req.body); + if (!parsed.success) { + return res.status(400).json({ error: parsed.error.flatten() }); + } + const db = await readDb(); + const index = db.profiles.findIndex((p) => p.id === req.params.profileId); + if (index === -1) { + return res.status(404).json({ error: "Profile not found" }); + } + db.profiles[index] = { + ...db.profiles[index], + ...parsed.data, + }; + await writeDb(db); + res.json(db.profiles[index]); +}); + +router.delete("/:profileId", async (req, res) => { + const db = await readDb(); + const next = db.profiles.filter((p) => p.id !== req.params.profileId); + if (next.length === db.profiles.length) { + return res.status(404).json({ error: "Profile not found" }); + } + db.profiles = next; + await writeDb(db); + res.json({ ok: true }); +}); + +router.post("/apply", async (req, res) => { + const { profileId, hash } = req.body ?? {}; + const db = await readDb(); + const profile = db.profiles.find((p) => p.id === profileId); + if (!profile) { + return res.status(404).json({ error: "Profile not found" }); + } + res.json({ + hash, + allowIp: profile.allowIp, + delayMs: profile.delayMs, + targetLoops: profile.targetLoops, + }); +}); + +export default router; diff --git a/apps/server/src/qbit/qbit.capabilities.ts b/apps/server/src/qbit/qbit.capabilities.ts new file mode 100644 index 0000000..2081a5b --- /dev/null +++ b/apps/server/src/qbit/qbit.capabilities.ts @@ -0,0 +1,30 @@ +import { QbitClient } from "./qbit.client" +import { QbitCapabilities } from "./qbit.types" + +export const detectCapabilities = async ( + client: QbitClient +): Promise => { + const version = await client.getVersion(); + let hasPeersEndpoint = true; + let hasBanEndpoint = true; + + try { + await client.getTorrentPeers("__probe__"); + } catch (error) { + const status = (error as any)?.response?.status; + if (status !== 400 && status !== 404) { + hasPeersEndpoint = false; + } + } + + try { + await client.banPeers(["127.0.0.1:1"]); + } catch (error) { + const status = (error as any)?.response?.status; + if (status !== 400 && status !== 404) { + hasBanEndpoint = false; + } + } + + return { version, hasPeersEndpoint, hasBanEndpoint }; +}; diff --git a/apps/server/src/qbit/qbit.client.ts b/apps/server/src/qbit/qbit.client.ts new file mode 100644 index 0000000..ba397b4 --- /dev/null +++ b/apps/server/src/qbit/qbit.client.ts @@ -0,0 +1,177 @@ +import axios, { AxiosInstance } from "axios"; +import { CookieJar } from "tough-cookie"; +import { wrapper } from "axios-cookiejar-support"; +import FormData from "form-data"; +import fs from "node:fs"; +import { config } from "../config" +import { logger } from "../utils/logger" +import { + QbitPeerList, + QbitTorrentInfo, + QbitTorrentProperties, + QbitTransferInfo, +} from "./qbit.types" + +export class QbitClient { + private client: AxiosInstance; + private jar: CookieJar; + private loggedIn = false; + + constructor() { + this.jar = new CookieJar(); + this.client = wrapper( + axios.create({ + baseURL: config.qbitBaseUrl, + jar: this.jar, + withCredentials: true, + }) + ); + } + + async login(): Promise { + if (!config.qbitBaseUrl) { + throw new Error("QBIT_BASE_URL missing"); + } + const form = new URLSearchParams(); + form.append("username", config.qbitUsername); + form.append("password", config.qbitPassword); + await this.client.post("/api/v2/auth/login", form, { + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + }); + this.loggedIn = true; + } + + private async request(fn: () => Promise): Promise { + try { + if (!this.loggedIn) { + await this.login(); + } + return await fn(); + } catch (error) { + if ( + axios.isAxiosError(error) && + (error.response?.status === 401 || error.response?.status === 403) + ) { + logger.warn("qBittorrent session expired, re-login"); + this.loggedIn = false; + await this.login(); + return await fn(); + } + throw error; + } + } + + async getVersion(): Promise { + const response = await this.request(() => + this.client.get("/api/v2/app/version") + ); + return response.data; + } + + async getTorrentsInfo(): Promise { + const response = await this.request(() => + this.client.get("/api/v2/torrents/info", { + params: { filter: "all" }, + }) + ); + return response.data; + } + + async getTransferInfo(): Promise { + const response = await this.request(() => + this.client.get("/api/v2/transfer/info") + ); + return response.data; + } + + async getTorrentProperties(hash: string): Promise { + const response = await this.request(() => + this.client.get("/api/v2/torrents/properties", { + params: { hash }, + }) + ); + return response.data; + } + + async getTorrentPeers(hash: string): Promise { + const response = await this.request(() => + this.client.get("/api/v2/sync/torrentPeers", { + params: { hash }, + }) + ); + return response.data; + } + + async exportTorrent(hash: string): Promise { + const response = await this.request(() => + this.client.get("/api/v2/torrents/export", { + params: { hashes: hash }, + responseType: "arraybuffer", + }) + ); + return Buffer.from(response.data); + } + + async addTorrentByMagnet(magnet: string, options: Record = {}) { + const form = new URLSearchParams(); + form.append("urls", magnet); + Object.entries(options).forEach(([key, value]) => form.append(key, value)); + await this.request(() => + this.client.post("/api/v2/torrents/add", form, { + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + }) + ); + } + + async addTorrentByFile(filePath: string, options: Record = {}) { + const form = new FormData(); + form.append("torrents", fs.createReadStream(filePath)); + Object.entries(options).forEach(([key, value]) => form.append(key, value)); + await this.request(() => + this.client.post("/api/v2/torrents/add", form, { + headers: form.getHeaders(), + }) + ); + } + + async deleteTorrent(hash: string, deleteFiles = true) { + const form = new URLSearchParams(); + form.append("hashes", hash); + form.append("deleteFiles", deleteFiles ? "true" : "false"); + await this.request(() => + this.client.post("/api/v2/torrents/delete", form, { + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + }) + ); + } + + async pauseTorrent(hash: string) { + const form = new URLSearchParams(); + form.append("hashes", hash); + await this.request(() => + this.client.post("/api/v2/torrents/pause", form, { + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + }) + ); + } + + async resumeTorrent(hash: string) { + const form = new URLSearchParams(); + form.append("hashes", hash); + await this.request(() => + this.client.post("/api/v2/torrents/resume", form, { + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + }) + ); + } + + async banPeers(peers: string[]) { + const form = new URLSearchParams(); + form.append("peers", peers.join("|")); + await this.request(() => + this.client.post("/api/v2/transfer/banPeers", form, { + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + }) + ); + } +} diff --git a/apps/server/src/qbit/qbit.context.ts b/apps/server/src/qbit/qbit.context.ts new file mode 100644 index 0000000..c30b385 --- /dev/null +++ b/apps/server/src/qbit/qbit.context.ts @@ -0,0 +1,22 @@ +import { QbitClient } from "./qbit.client" +import { QbitCapabilities } from "./qbit.types" + +let client: QbitClient | null = null; +let capabilities: QbitCapabilities | null = null; + +export const setQbitClient = (instance: QbitClient) => { + client = instance; +}; + +export const getQbitClient = () => { + if (!client) { + throw new Error("Qbit client not initialized"); + } + return client; +}; + +export const setQbitCapabilities = (caps: QbitCapabilities) => { + capabilities = caps; +}; + +export const getQbitCapabilities = () => capabilities; diff --git a/apps/server/src/qbit/qbit.routes.ts b/apps/server/src/qbit/qbit.routes.ts new file mode 100644 index 0000000..bc322a0 --- /dev/null +++ b/apps/server/src/qbit/qbit.routes.ts @@ -0,0 +1,30 @@ +import { Router } from "express"; +import { getQbitClient } from "./qbit.context" + +const router = Router(); + +router.get("/torrents", async (_req, res) => { + const qbit = getQbitClient(); + const torrents = await qbit.getTorrentsInfo(); + res.json(torrents); +}); + +router.get("/transfer", async (_req, res) => { + const qbit = getQbitClient(); + const transfer = await qbit.getTransferInfo(); + res.json(transfer); +}); + +router.get("/torrent/:hash", async (req, res) => { + const qbit = getQbitClient(); + const props = await qbit.getTorrentProperties(req.params.hash); + res.json(props); +}); + +router.delete("/torrent/:hash", async (req, res) => { + const qbit = getQbitClient(); + await qbit.deleteTorrent(req.params.hash, true); + res.json({ ok: true }); +}); + +export default router; diff --git a/apps/server/src/qbit/qbit.types.ts b/apps/server/src/qbit/qbit.types.ts new file mode 100644 index 0000000..c53edd2 --- /dev/null +++ b/apps/server/src/qbit/qbit.types.ts @@ -0,0 +1,51 @@ +export interface QbitTorrentInfo { + hash: string; + name: string; + size: number; + progress: number; + dlspeed: number; + state: string; + magnet_uri?: string; + completed?: number; + tags?: string; + category?: string; + tracker?: string; + seeding_time?: number; + uploaded?: number; +} + +export interface QbitTransferInfo { + dl_info_speed: number; + dl_info_data: number; + up_info_speed: number; + up_info_data: number; + connection_status: string; +} + +export interface QbitTorrentProperties { + save_path?: string; + completion_on?: number; + comment?: string; + total_size?: number; + piece_size?: number; +} + +export interface QbitPeerList { + peers: Record< + string, + { + ip: string; + port: number; + client: string; + progress: number; + dl_speed: number; + up_speed: number; + } + >; +} + +export interface QbitCapabilities { + version: string; + hasPeersEndpoint: boolean; + hasBanEndpoint: boolean; +} diff --git a/apps/server/src/realtime/emitter.ts b/apps/server/src/realtime/emitter.ts new file mode 100644 index 0000000..01bc0f5 --- /dev/null +++ b/apps/server/src/realtime/emitter.ts @@ -0,0 +1,43 @@ +import { Server } from "socket.io"; +import { EVENTS } from "./events"; +import { StatusSnapshot } from "../status/status.service"; +import { LoopJob, TimerLog, TimerSummary } from "../types"; + +let io: Server | null = null; + +export const initEmitter = (server: Server) => { + io = server; +}; + +export const emitStatusSnapshot = (snapshot: StatusSnapshot) => { + io?.emit(EVENTS.STATUS_SNAPSHOT, snapshot); +}; + +export const emitStatusUpdate = (snapshot: StatusSnapshot) => { + io?.emit(EVENTS.STATUS_UPDATE, snapshot); +}; + +export const emitJobMetrics = (job: LoopJob) => { + io?.emit(EVENTS.JOB_METRICS, job); +}; + +export const emitJobLog = (payload: { + jobId: string; + level: "INFO" | "WARN" | "ERROR"; + message: string; + createdAt: string; +}) => { + io?.emit(EVENTS.JOB_LOG, payload); +}; + +export const emitQbitHealth = (payload: StatusSnapshot["qbit"]) => { + io?.emit(EVENTS.QBIT_HEALTH, payload); +}; + +export const emitTimerLog = (payload: TimerLog) => { + io?.emit(EVENTS.TIMER_LOG, payload); +}; + +export const emitTimerSummary = (payload: TimerSummary) => { + io?.emit(EVENTS.TIMER_SUMMARY, payload); +}; diff --git a/apps/server/src/realtime/events.ts b/apps/server/src/realtime/events.ts new file mode 100644 index 0000000..3589dca --- /dev/null +++ b/apps/server/src/realtime/events.ts @@ -0,0 +1,9 @@ +export const EVENTS = { + STATUS_SNAPSHOT: "status:snapshot", + STATUS_UPDATE: "status:update", + JOB_METRICS: "job:metrics", + JOB_LOG: "job:log", + QBIT_HEALTH: "qbit:health", + TIMER_LOG: "timer:log", + TIMER_SUMMARY: "timer:summary", +}; diff --git a/apps/server/src/realtime/socket.ts b/apps/server/src/realtime/socket.ts new file mode 100644 index 0000000..6126f3b --- /dev/null +++ b/apps/server/src/realtime/socket.ts @@ -0,0 +1,59 @@ +import { Server } from "socket.io"; +import http from "node:http"; +import { verifyToken } from "../auth/auth.service"; +import { getStatusSnapshot } from "../status/status.service"; +import { EVENTS } from "./events"; +import { config, isDev } from "../config"; + +const parseCookies = (cookieHeader?: string) => { + const cookies: Record = {}; + if (!cookieHeader) { + return cookies; + } + cookieHeader.split(";").forEach((part) => { + const [key, ...rest] = part.trim().split("="); + cookies[key] = decodeURIComponent(rest.join("=")); + }); + return cookies; +}; + +export const createSocketServer = (server: http.Server) => { + const io = new Server(server, { + cors: isDev + ? { + origin: true, + credentials: true, + } + : undefined, + }); + + io.use((socket, next) => { + const authToken = socket.handshake.auth?.token as string | undefined; + if (authToken) { + try { + verifyToken(authToken); + return next(); + } catch (error) { + return next(new Error("Unauthorized")); + } + } + const cookies = parseCookies(socket.request.headers.cookie); + const token = cookies["qbuffer_token"]; + if (!token) { + return next(new Error("Unauthorized")); + } + try { + verifyToken(token); + return next(); + } catch (error) { + return next(new Error("Unauthorized")); + } + }); + + io.on("connection", async (socket) => { + const snapshot = await getStatusSnapshot(); + socket.emit(EVENTS.STATUS_SNAPSHOT, snapshot); + }); + + return io; +}; diff --git a/apps/server/src/status/status.routes.ts b/apps/server/src/status/status.routes.ts new file mode 100644 index 0000000..967273a --- /dev/null +++ b/apps/server/src/status/status.routes.ts @@ -0,0 +1,11 @@ +import { Router } from "express"; +import { getStatusSnapshot } from "./status.service" + +const router = Router(); + +router.get("/", async (_req, res) => { + const snapshot = await getStatusSnapshot(); + res.json(snapshot); +}); + +export default router; diff --git a/apps/server/src/status/status.service.ts b/apps/server/src/status/status.service.ts new file mode 100644 index 0000000..ab72c91 --- /dev/null +++ b/apps/server/src/status/status.service.ts @@ -0,0 +1,45 @@ +import { DbSchema, LoopJob } from "../types" +import { QbitCapabilities, QbitTorrentInfo, QbitTransferInfo } from "../qbit/qbit.types" +import { readDb } from "../storage/jsondb" + +export interface StatusSnapshot { + qbit: { + ok: boolean; + version?: string; + capabilities?: QbitCapabilities; + lastError?: string; + }; + torrents: QbitTorrentInfo[]; + transfer?: QbitTransferInfo; + jobs: LoopJob[]; +} + +let snapshot: StatusSnapshot = { + qbit: { ok: false }, + torrents: [], + transfer: undefined, + jobs: [], +}; + +export const setQbitStatus = (status: StatusSnapshot["qbit"]) => { + snapshot.qbit = status; +}; + +export const setTorrentsStatus = ( + torrents: QbitTorrentInfo[], + transfer?: QbitTransferInfo +) => { + snapshot.torrents = torrents; + snapshot.transfer = transfer; +}; + +export const refreshJobsStatus = async () => { + const db: DbSchema = await readDb(); + snapshot.jobs = db.loopJobs; + return snapshot.jobs; +}; + +export const getStatusSnapshot = async (): Promise => { + await refreshJobsStatus(); + return snapshot; +}; diff --git a/apps/server/src/storage/jsondb.ts b/apps/server/src/storage/jsondb.ts new file mode 100644 index 0000000..a5cab65 --- /dev/null +++ b/apps/server/src/storage/jsondb.ts @@ -0,0 +1,90 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { config } from "../config" +import { DbSchema } from "../types" +import { Mutex } from "./mutex" + +const mutex = new Mutex(); + +const defaultDb = (): DbSchema => ({ + users: [], + settings: { + pollIntervalMs: config.pollIntervalMs, + enforceIntervalMs: config.enforceIntervalMs, + defaultDelayMs: config.defaultDelayMs, + maxLoopLimit: config.maxLoopLimit, + }, + loopJobs: [], + profiles: [], + auditLogs: [], + archives: {}, + timerRules: [], + timerLogs: [], + timerSummary: { + totalDeleted: 0, + totalSeededSeconds: 0, + totalUploadedBytes: 0, + updatedAt: new Date().toISOString(), + }, +}); + +const rotateBackups = async (dbPath: string) => { + const dir = path.dirname(dbPath); + const base = path.basename(dbPath); + for (let i = 2; i >= 0; i -= 1) { + const src = path.join(dir, `${base}.bak${i}`); + const dest = path.join(dir, `${base}.bak${i + 1}`); + try { + await fs.rename(src, dest); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + throw error; + } + } + } + try { + await fs.copyFile(dbPath, path.join(dir, `${base}.bak0`)); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + throw error; + } + } +}; + +const writeRaw = async (data: DbSchema) => { + await rotateBackups(config.dbPath); + const tempPath = `${config.dbPath}.tmp`; + await fs.writeFile(tempPath, JSON.stringify(data, null, 2), "utf-8"); + await fs.rename(tempPath, config.dbPath); +}; + +export const readDb = async (): Promise => { + return mutex.run(async () => { + try { + const content = await fs.readFile(config.dbPath, "utf-8"); + const parsed = JSON.parse(content) as DbSchema; + parsed.timerRules ??= []; + parsed.timerLogs ??= []; + parsed.timerSummary ??= { + totalDeleted: 0, + totalSeededSeconds: 0, + totalUploadedBytes: 0, + updatedAt: new Date().toISOString(), + }; + return parsed; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + throw error; + } + const initial = defaultDb(); + await writeRaw(initial); + return initial; + } + }); +}; + +export const writeDb = async (data: DbSchema): Promise => { + await mutex.run(async () => { + await writeRaw(data); + }); +}; diff --git a/apps/server/src/storage/mutex.ts b/apps/server/src/storage/mutex.ts new file mode 100644 index 0000000..27189cc --- /dev/null +++ b/apps/server/src/storage/mutex.ts @@ -0,0 +1,12 @@ +export class Mutex { + private current: Promise = Promise.resolve(); + + async run(fn: () => Promise): Promise { + const next = this.current.then(fn, fn); + this.current = next.then( + () => undefined, + () => undefined + ); + return next; + } +} diff --git a/apps/server/src/storage/paths.ts b/apps/server/src/storage/paths.ts new file mode 100644 index 0000000..c0f74a2 --- /dev/null +++ b/apps/server/src/storage/paths.ts @@ -0,0 +1,7 @@ +import fs from "node:fs/promises"; +import { config } from "../config" + +export const ensureDataPaths = async () => { + await fs.mkdir(config.dataDir, { recursive: true }); + await fs.mkdir(config.torrentArchiveDir, { recursive: true }); +}; diff --git a/apps/server/src/timer/timer.routes.ts b/apps/server/src/timer/timer.routes.ts new file mode 100644 index 0000000..2c91d4a --- /dev/null +++ b/apps/server/src/timer/timer.routes.ts @@ -0,0 +1,58 @@ +import { Router } from "express"; +import { randomUUID } from "node:crypto"; +import { readDb, writeDb } from "../storage/jsondb"; +import { TimerRule } from "../types"; +import { nowIso } from "../utils/time"; +import { z } from "zod"; + +const router = Router(); + +const ruleSchema = z.object({ + tags: z.array(z.string().min(1)).min(1), + seedLimitSeconds: z.number().int().min(60).max(60 * 60 * 24 * 365), +}); + +router.get("/rules", async (_req, res) => { + const db = await readDb(); + res.json(db.timerRules ?? []); +}); + +router.post("/rules", async (req, res) => { + const parsed = ruleSchema.safeParse(req.body); + if (!parsed.success) { + return res.status(400).json({ error: parsed.error.flatten() }); + } + const db = await readDb(); + const rule: TimerRule = { + id: randomUUID(), + tags: parsed.data.tags, + seedLimitSeconds: parsed.data.seedLimitSeconds, + createdAt: nowIso(), + }; + db.timerRules = [...(db.timerRules ?? []), rule]; + await writeDb(db); + res.json(rule); +}); + +router.delete("/rules/:ruleId", async (req, res) => { + const db = await readDb(); + const next = (db.timerRules ?? []).filter((rule) => rule.id !== req.params.ruleId); + if (next.length === (db.timerRules ?? []).length) { + return res.status(404).json({ error: "Rule not found" }); + } + db.timerRules = next; + await writeDb(db); + res.json({ ok: true }); +}); + +router.get("/logs", async (_req, res) => { + const db = await readDb(); + res.json(db.timerLogs ?? []); +}); + +router.get("/summary", async (_req, res) => { + const db = await readDb(); + res.json(db.timerSummary ?? null); +}); + +export default router; diff --git a/apps/server/src/timer/timer.types.ts b/apps/server/src/timer/timer.types.ts new file mode 100644 index 0000000..c9cdfba --- /dev/null +++ b/apps/server/src/timer/timer.types.ts @@ -0,0 +1,4 @@ +export interface TimerRuleInput { + tags: string[]; + seedLimitSeconds: number; +} diff --git a/apps/server/src/timer/timer.worker.ts b/apps/server/src/timer/timer.worker.ts new file mode 100644 index 0000000..963e6b1 --- /dev/null +++ b/apps/server/src/timer/timer.worker.ts @@ -0,0 +1,94 @@ +import { randomUUID } from "node:crypto"; +import { QbitClient } from "../qbit/qbit.client"; +import { readDb, writeDb } from "../storage/jsondb"; +import { TimerLog, TimerSummary } from "../types"; +import { emitTimerLog, emitTimerSummary } from "../realtime/emitter"; +import { nowIso } from "../utils/time"; + +const MAX_LOGS = 2000; + +const normalizeTags = (tags?: string, category?: string) => { + const tagList = tags ? tags.split(",").map((tag) => tag.trim()).filter(Boolean) : []; + if (category) { + tagList.push(category); + } + return Array.from(new Set(tagList.map((tag) => tag.toLowerCase()))); +}; + +export const startTimerWorker = (qbit: QbitClient, intervalMs: number) => { + setInterval(async () => { + const db = await readDb(); + const rules = db.timerRules ?? []; + if (rules.length === 0) { + return; + } + const torrents = await qbit.getTorrentsInfo(); + let summary: TimerSummary = + db.timerSummary ?? { + totalDeleted: 0, + totalSeededSeconds: 0, + totalUploadedBytes: 0, + updatedAt: nowIso(), + }; + + const logs: TimerLog[] = []; + + for (const torrent of torrents) { + const tags = normalizeTags(torrent.tags, torrent.category); + const addedOnMs = Number(torrent.added_on ?? 0) * 1000; + const matchingRules = rules.filter((rule) => { + const ruleCreatedAtMs = Date.parse(rule.createdAt); + if (Number.isFinite(ruleCreatedAtMs) && addedOnMs > 0) { + if (addedOnMs < ruleCreatedAtMs) { + return false; + } + } + return rule.tags.some((tag) => tags.includes(tag.toLowerCase())); + }); + if (matchingRules.length === 0) { + continue; + } + const matchingRule = matchingRules.reduce((best, current) => + current.seedLimitSeconds < best.seedLimitSeconds ? current : best + ); + const seedingSeconds = Number(torrent.seeding_time ?? 0); + if (seedingSeconds < matchingRule.seedLimitSeconds) { + continue; + } + + try { + await qbit.deleteTorrent(torrent.hash, true); + } catch (error) { + continue; + } + + const logEntry: TimerLog = { + id: randomUUID(), + hash: torrent.hash, + name: torrent.name, + sizeBytes: torrent.size, + tracker: torrent.tracker, + tags, + category: torrent.category, + seedingTimeSeconds: seedingSeconds, + uploadedBytes: torrent.uploaded ?? 0, + deletedAt: nowIso(), + }; + logs.push(logEntry); + summary = { + totalDeleted: summary.totalDeleted + 1, + totalSeededSeconds: summary.totalSeededSeconds + seedingSeconds, + totalUploadedBytes: summary.totalUploadedBytes + (torrent.uploaded ?? 0), + updatedAt: nowIso(), + }; + emitTimerLog(logEntry); + emitTimerSummary(summary); + } + + if (logs.length > 0) { + db.timerLogs = [...(db.timerLogs ?? []), ...logs].slice(-MAX_LOGS); + db.timerSummary = summary; + await writeDb(db); + } + }, intervalMs); +}; diff --git a/apps/server/src/torrent/torrent.archive.ts b/apps/server/src/torrent/torrent.archive.ts new file mode 100644 index 0000000..9c77d74 --- /dev/null +++ b/apps/server/src/torrent/torrent.archive.ts @@ -0,0 +1,46 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { readDb, writeDb } from "../storage/jsondb"; +import { ArchiveStatus } from "../types"; +import { nowIso } from "../utils/time"; +import { config } from "../config"; + +export const setArchiveStatus = async (status: ArchiveStatus) => { + const db = await readDb(); + db.archives[status.hash] = status; + await writeDb(db); + return status; +}; + +export const getArchiveStatus = async (hash: string) => { + const db = await readDb(); + const existing = db.archives[hash]; + const torrentPath = path.join(config.torrentArchiveDir, `${hash}.torrent`); + try { + await fs.access(torrentPath); + if (!existing || existing.status !== "READY") { + const updated: ArchiveStatus = { + hash, + status: "READY", + torrentFilePath: torrentPath, + source: existing?.source ?? "manual", + updatedAt: nowIso(), + }; + db.archives[hash] = updated; + await writeDb(db); + return updated; + } + } catch (error) { + // File does not exist; fall back to stored status. + } + return existing; +}; + +export const createPendingArchive = async (hash: string) => { + const status: ArchiveStatus = { + hash, + status: "PENDING", + updatedAt: nowIso(), + }; + return setArchiveStatus(status); +}; diff --git a/apps/server/src/torrent/torrent.generator.ts b/apps/server/src/torrent/torrent.generator.ts new file mode 100644 index 0000000..5dc2cd7 --- /dev/null +++ b/apps/server/src/torrent/torrent.generator.ts @@ -0,0 +1,41 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { config } from "../config"; +import { logger } from "../utils/logger"; + +export const generateTorrentFile = async ( + magnet: string, + hash: string +): Promise => { + const targetPath = path.join(config.torrentArchiveDir, `${hash}.torrent`); + const { default: WebTorrent } = await import("webtorrent"); + const client = new WebTorrent(); + + return new Promise((resolve, reject) => { + const torrent = client.add(magnet, { path: config.dataDir }); + const timeout = setTimeout(() => { + client.destroy(); + reject(new Error("Metadata fetch timeout")); + }, 120_000); + + torrent.on("metadata", async () => { + clearTimeout(timeout); + try { + const buffer = torrent.torrentFile; + await fs.writeFile(targetPath, buffer); + resolve(targetPath); + } catch (error) { + reject(error); + } finally { + client.destroy(); + } + }); + + torrent.on("error", (error) => { + logger.error({ error }, "Torrent metadata error"); + clearTimeout(timeout); + client.destroy(); + reject(error); + }); + }); +}; diff --git a/apps/server/src/torrent/torrent.routes.ts b/apps/server/src/torrent/torrent.routes.ts new file mode 100644 index 0000000..99347fb --- /dev/null +++ b/apps/server/src/torrent/torrent.routes.ts @@ -0,0 +1,121 @@ +import { Router } from "express"; +import multer from "multer"; +import path from "node:path"; +import fs from "node:fs/promises"; +import { getQbitClient } from "../qbit/qbit.context"; +import { getArchiveStatus, setArchiveStatus } from "./torrent.archive"; +import { nowIso } from "../utils/time"; +import { appendAuditLog, logger } from "../utils/logger"; +import { config } from "../config"; + +const router = Router(); +const upload = multer({ dest: "/tmp" }); + +router.post("/select", async (req, res) => { + const { hash } = req.body ?? {}; + if (!hash) { + return res.status(400).json({ error: "Missing hash" }); + } + const existing = await getArchiveStatus(hash); + if (existing?.status === "READY") { + return res.json({ ok: true, hash, archive: existing }); + } + const qbit = getQbitClient(); + const torrents = await qbit.getTorrentsInfo(); + const torrent = torrents.find((t) => t.hash === hash); + if (!torrent) { + return res.status(404).json({ error: "Torrent not found" }); + } + + await setArchiveStatus({ + hash, + status: "MISSING", + updatedAt: nowIso(), + }); + + res.json({ ok: true, hash, archive: { status: "MISSING" } }); +}); + +router.post("/archive/from-selected", async (req, res) => { + const { hash } = req.body ?? {}; + if (!hash) { + return res.status(400).json({ error: "Missing hash" }); + } + const existing = await getArchiveStatus(hash); + if (existing?.status === "READY") { + return res.json({ ok: true, torrentFilePath: existing.torrentFilePath, source: existing.source }); + } + await setArchiveStatus({ + hash, + status: "MISSING", + lastError: "Magnet export disabled; upload .torrent manually.", + updatedAt: nowIso(), + }); + await appendAuditLog({ + level: "WARN", + event: "ARCHIVE_FAIL", + message: `Archive generation disabled for ${hash}; manual upload required`, + }); + return res.status(400).json({ error: "Magnet export disabled; upload .torrent manually." }); +}); + +router.post("/archive/upload", upload.single("file"), async (req, res) => { + const { hash } = req.body ?? {}; + if (!hash || !req.file) { + return res.status(400).json({ error: "Missing hash or file" }); + } + const inputHash = String(hash).toLowerCase(); + const buffer = await fs.readFile(req.file.path); + let warning: string | undefined; + try { + const { default: parseTorrent } = await import("parse-torrent"); + const parsed = parseTorrent(buffer); + const infoHash = String(parsed.infoHash ?? "").toLowerCase(); + if (infoHash && infoHash !== inputHash) { + await fs.unlink(req.file.path); + return res.status(400).json({ + error: "Torrent hash uyuşmuyor. Doğru .torrent dosyasını seçin.", + expected: inputHash, + actual: infoHash, + }); + } + } catch (error) { + warning = "Torrent dosyası okunamadı; yine de arşive kaydedildi."; + logger.warn({ error, hash: inputHash }, "Torrent parse failed; storing archive anyway"); + } + const targetPath = path.join(config.torrentArchiveDir, `${hash}.torrent`); + await fs.writeFile(targetPath, buffer); + await fs.unlink(req.file.path); + await setArchiveStatus({ + hash, + status: "READY", + torrentFilePath: targetPath, + source: "manual", + updatedAt: nowIso(), + }); + try { + const qbit = getQbitClient(); + await qbit.addTorrentByFile(targetPath); + return res.json({ + ok: true, + torrentFilePath: targetPath, + added: true, + }); + } catch (error) { + return res.json({ + ok: true, + torrentFilePath: targetPath, + added: false, + }); + } +}); + +router.get("/archive/status/:hash", async (req, res) => { + const status = await getArchiveStatus(req.params.hash); + if (!status) { + return res.json({ hash: req.params.hash, status: "MISSING" }); + } + return res.json(status); +}); + +export default router; diff --git a/apps/server/src/types.ts b/apps/server/src/types.ts new file mode 100644 index 0000000..2ce8ca9 --- /dev/null +++ b/apps/server/src/types.ts @@ -0,0 +1,131 @@ +export type LoopStatus = + | "IDLE" + | "RUNNING" + | "WAITING_DELAY" + | "STOPPED" + | "ERROR" + | "COMPLETED"; + +export type EnforcementMode = "aggressive-soft"; + +export interface User { + username: string; + passwordHash: string; + createdAt: string; +} + +export interface Settings { + pollIntervalMs: number; + enforceIntervalMs: number; + defaultDelayMs: number; + maxLoopLimit: number; +} + +export interface LoopJob { + id: string; + torrentHash: string; + name: string; + sizeBytes: number; + magnet?: string; + torrentFilePath?: string; + allowIp: string; + targetLoops: number; + doneLoops: number; + delayMs: number; + deleteDataBetweenLoops: boolean; + enforcementMode: EnforcementMode; + status: LoopStatus; + currentRun?: { + startedAt: string; + lastProgress: number; + lastProgressAt?: string; + stalledSince?: string; + downloadedThisRunBytes: number; + avgSpeed: number; + }; + totals: { + totalDownloadedBytes: number; + totalTimeMs: number; + }; + bans: { + bannedIps: string[]; + lastBanAt?: string; + }; + nextRunAt?: string; + createdAt: string; + updatedAt: string; + lastError?: string; +} + +export interface Profile { + id: string; + name: string; + allowIp: string; + delayMs: number; + targetLoops: number; + createdAt: string; +} + +export interface TimerRule { + id: string; + tags: string[]; + seedLimitSeconds: number; + createdAt: string; +} + +export interface TimerLog { + id: string; + hash: string; + name: string; + sizeBytes: number; + tracker?: string; + tags: string[]; + category?: string; + seedingTimeSeconds: number; + uploadedBytes: number; + deletedAt: string; +} + +export interface TimerSummary { + totalDeleted: number; + totalSeededSeconds: number; + totalUploadedBytes: number; + updatedAt: string; +} + +export interface AuditLog { + id: string; + level: "INFO" | "WARN" | "ERROR"; + event: + | "JOB_STARTED" + | "JOB_COMPLETED_LOOP" + | "JOB_COMPLETED_ALL" + | "JOB_RESTARTED" + | "PEER_BANNED" + | "QBIT_RELOGIN" + | "ARCHIVE_SUCCESS" + | "ARCHIVE_FAIL"; + message: string; + createdAt: string; +} + +export interface ArchiveStatus { + hash: string; + status: "PENDING" | "READY" | "FAILED" | "MISSING"; + torrentFilePath?: string; + source?: "manual" | "generated" | "exported"; + lastError?: string; + updatedAt: string; +} + +export interface DbSchema { + users: User[]; + settings: Settings; + loopJobs: LoopJob[]; + profiles: Profile[]; + auditLogs: AuditLog[]; + archives: Record; + timerRules?: TimerRule[]; + timerLogs?: TimerLog[]; + timerSummary?: TimerSummary; +} diff --git a/apps/server/src/utils/logger.ts b/apps/server/src/utils/logger.ts new file mode 100644 index 0000000..7f7f6e6 --- /dev/null +++ b/apps/server/src/utils/logger.ts @@ -0,0 +1,34 @@ +import fs from "node:fs/promises"; +import { randomUUID } from "node:crypto"; +import pino from "pino"; +import { config } from "../config" +import { AuditLog } from "../types" + +export const logger = pino({ + level: process.env.LOG_LEVEL ?? "info", +}); + +export const appendAuditLog = async ( + entry: Omit +) => { + const logEntry: AuditLog = { + id: randomUUID(), + createdAt: new Date().toISOString(), + ...entry, + }; + try { + let existing: AuditLog[] = []; + try { + const content = await fs.readFile(config.logsPath, "utf-8"); + existing = JSON.parse(content) as AuditLog[]; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + throw error; + } + } + const next = [...existing, logEntry].slice(-2000); + await fs.writeFile(config.logsPath, JSON.stringify(next, null, 2), "utf-8"); + } catch (error) { + logger.error({ error }, "Failed to append audit log"); + } +}; diff --git a/apps/server/src/utils/time.ts b/apps/server/src/utils/time.ts new file mode 100644 index 0000000..ebbff9c --- /dev/null +++ b/apps/server/src/utils/time.ts @@ -0,0 +1,7 @@ +export const nowIso = () => new Date().toISOString(); + +export const secondsBetween = (startIso: string, endIso: string) => { + const start = new Date(startIso).getTime(); + const end = new Date(endIso).getTime(); + return Math.max(0, (end - start) / 1000); +}; diff --git a/apps/server/src/utils/validators.ts b/apps/server/src/utils/validators.ts new file mode 100644 index 0000000..824b19f --- /dev/null +++ b/apps/server/src/utils/validators.ts @@ -0,0 +1,25 @@ +import { z } from "zod"; + +export const allowIpSchema = z + .string() + .ip({ version: "v4" }) + .or(z.string().ip({ version: "v6" })); + +export const loopStartSchema = z.object({ + hash: z.string().min(1), + allowIp: allowIpSchema, + targetLoops: z.number().int().min(1).max(1000), + delayMs: z.number().int().min(0).max(86_400_000), +}); + +export const dryRunSchema = z.object({ + hash: z.string().min(1), + allowIp: allowIpSchema, +}); + +export const profileSchema = z.object({ + name: z.string().min(1).max(64), + allowIp: allowIpSchema, + delayMs: z.number().int().min(0).max(86_400_000), + targetLoops: z.number().int().min(1).max(1000), +}); diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json new file mode 100644 index 0000000..a9a931c --- /dev/null +++ b/apps/server/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "moduleResolution": "Node", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["src"] +} diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile new file mode 100644 index 0000000..885b4e7 --- /dev/null +++ b/apps/web/Dockerfile @@ -0,0 +1,10 @@ +FROM node:20-alpine +WORKDIR /app +RUN corepack enable +COPY package.json /app/apps/web/package.json +COPY package.json /app/package.json +COPY pnpm-workspace.yaml /app/pnpm-workspace.yaml +RUN pnpm install --frozen-lockfile=false +WORKDIR /app/apps/web +EXPOSE 5173 +CMD ["pnpm", "dev", "--host", "0.0.0.0"] diff --git a/apps/web/index.html b/apps/web/index.html new file mode 100644 index 0000000..2a0d4cd --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,19 @@ + + + + + + q-buffer + + + +
+ + + diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..ba51247 --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,33 @@ +{ + "name": "q-buffer-web", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.6.0", + "@fortawesome/free-solid-svg-icons": "^6.6.0", + "@fortawesome/react-fontawesome": "^0.2.2", + "@radix-ui/react-alert-dialog": "^1.1.2", + "axios": "^1.7.7", + "clsx": "^2.1.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.2", + "socket.io-client": "^4.7.5", + "zustand": "^4.5.5" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.39", + "tailwindcss": "^3.4.6", + "typescript": "^5.5.3", + "vite": "^5.3.3" + } +} diff --git a/apps/web/postcss.config.cjs b/apps/web/postcss.config.cjs new file mode 100644 index 0000000..5cbc2c7 --- /dev/null +++ b/apps/web/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx new file mode 100644 index 0000000..ad0efe2 --- /dev/null +++ b/apps/web/src/App.tsx @@ -0,0 +1,33 @@ +import React, { useEffect } from "react"; +import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; +import { LoginPage } from "./pages/LoginPage"; +import { DashboardPage } from "./pages/DashboardPage"; +import { TimerPage } from "./pages/TimerPage"; +import { AppLayout } from "./components/layout/AppLayout"; +import { useAuthStore } from "./store/useAuthStore"; + +export const App = () => { + const username = useAuthStore((s) => s.username); + const check = useAuthStore((s) => s.check); + + useEffect(() => { + check(); + }, [check]); + + if (!username) { + return ; + } + + return ( + + + + } /> + } /> + } /> + } /> + + + + ); +}; diff --git a/apps/web/src/api/client.ts b/apps/web/src/api/client.ts new file mode 100644 index 0000000..3ae367a --- /dev/null +++ b/apps/web/src/api/client.ts @@ -0,0 +1,8 @@ +import axios from "axios"; + +const baseURL = import.meta.env.VITE_API_BASE || ""; + +export const api = axios.create({ + baseURL: baseURL || undefined, + withCredentials: true, +}); diff --git a/apps/web/src/components/layout/AppLayout.tsx b/apps/web/src/components/layout/AppLayout.tsx new file mode 100644 index 0000000..acf08e6 --- /dev/null +++ b/apps/web/src/components/layout/AppLayout.tsx @@ -0,0 +1,137 @@ +import React, { useEffect, useState } from "react"; +import { NavLink } from "react-router-dom"; +import { Shell } from "./Shell"; +import { Badge } from "../ui/Badge"; +import { Button } from "../ui/Button"; +import { useAuthStore } from "../../store/useAuthStore"; +import { useAppStore } from "../../store/useAppStore"; +import { connectSocket } from "../../socket/socket"; +import { api } from "../../api/client"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faMoon, faSun } from "@fortawesome/free-solid-svg-icons"; +import { AlertToastStack } from "../ui/AlertToastStack"; + +export const AppLayout = ({ children }: { children: React.ReactNode }) => { + const logout = useAuthStore((s) => s.logout); + const qbit = useAppStore((s) => s.qbit); + const setSnapshot = useAppStore((s) => s.setSnapshot); + const [connected, setConnected] = useState(false); + const [theme, setTheme] = useState<"light" | "dark">("light"); + const [menuOpen, setMenuOpen] = useState(false); + + const applyTheme = (next: "light" | "dark") => { + document.documentElement.classList.toggle("dark", next === "dark"); + localStorage.setItem("theme", next); + setTheme(next); + }; + + useEffect(() => { + const stored = localStorage.getItem("theme"); + if (stored === "light" || stored === "dark") { + applyTheme(stored); + } else if (window.matchMedia?.("(prefers-color-scheme: dark)").matches) { + applyTheme("dark"); + } + const socket = connectSocket(); + socket.on("connect", () => setConnected(true)); + socket.on("disconnect", () => setConnected(false)); + return () => { + socket.disconnect(); + }; + }, []); + + useEffect(() => { + let active = true; + const fetchStatus = async () => { + try { + const response = await api.get("/api/status"); + if (active) { + setSnapshot(response.data); + } + } catch (error) { + if (active) { + // Ignore transient network errors; socket will update when available. + } + } + }; + fetchStatus(); + const interval = setInterval(fetchStatus, connected ? 15000 : 5000); + return () => { + active = false; + clearInterval(interval); + }; + }, [connected, setSnapshot]); + + return ( + +
+
+
+
q-buffer
+
+ qBittorrent {qbit.version ?? "unknown"} +
+
+ +
+
+ +
+ + {qbit.ok ? "Qbit OK" : "Qbit Down"} + + + {connected ? "Live" : "Offline"} + + + +
+
+
+ + {children} +
+ ); +}; diff --git a/apps/web/src/components/layout/Shell.tsx b/apps/web/src/components/layout/Shell.tsx new file mode 100644 index 0000000..7c7406f --- /dev/null +++ b/apps/web/src/components/layout/Shell.tsx @@ -0,0 +1,9 @@ +import React from "react"; + +export const Shell = ({ children }: { children: React.ReactNode }) => ( +
+
+ {children} +
+
+); diff --git a/apps/web/src/components/loop/AdvancedUploadCard.tsx b/apps/web/src/components/loop/AdvancedUploadCard.tsx new file mode 100644 index 0000000..293cd09 --- /dev/null +++ b/apps/web/src/components/loop/AdvancedUploadCard.tsx @@ -0,0 +1,111 @@ +import React, { useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "../ui/Card"; +import { Button } from "../ui/Button"; +import { api } from "../../api/client"; +import { useAppStore } from "../../store/useAppStore"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faCircleInfo, faTriangleExclamation, faUpload } from "@fortawesome/free-solid-svg-icons"; +import { useUiStore } from "../../store/useUiStore"; + +export const AdvancedUploadCard = () => { + const selectedHash = useAppStore((s) => s.selectedHash); + const [open, setOpen] = useState(false); + const [file, setFile] = useState(null); + const pushAlert = useUiStore((s) => s.pushAlert); + + const upload = async () => { + if (!selectedHash) { + pushAlert({ + title: "Torrent seçilmedi", + description: "Önce bir torrent seçmelisiniz.", + variant: "warn", + }); + return; + } + if (!file) { + pushAlert({ + title: "Dosya seçilmedi", + description: "Lütfen bir .torrent dosyası seçin.", + variant: "warn", + }); + return; + } + try { + const form = new FormData(); + form.append("file", file); + form.append("hash", selectedHash); + const response = await api.post("/api/torrent/archive/upload", form, { + headers: { "Content-Type": "multipart/form-data" }, + }); + if (response.data?.added) { + pushAlert({ + title: "Yükleme başarılı", + description: "Torrent qBittorrent'a eklendi.", + variant: "success", + }); + } else { + pushAlert({ + title: "Yükleme yapıldı", + description: "Torrent arşive alındı, qBittorrent'a eklenemedi.", + variant: "warn", + }); + } + if (selectedHash) { + const statusResponse = await api.get( + `/api/torrent/archive/status/${selectedHash}` + ); + window.dispatchEvent( + new CustomEvent("archive-status", { + detail: { hash: selectedHash, status: statusResponse.data?.status }, + }) + ); + } + } catch (error: any) { + const apiError = error?.response?.data?.error; + pushAlert({ + title: "Yükleme başarısız", + description: apiError || "Sunucu loglarını kontrol edin.", + variant: "error", + }); + } + }; + + return ( + + + + + Advanced + + + + {open && ( + +
+
+ + Upload a .torrent file if magnet metadata fetch fails. +
+ {!selectedHash && ( +
+ + Önce bir torrent seçmelisiniz. +
+ )} + setFile(e.target.files?.[0] ?? null)} + /> + +
+
+ )} +
+ ); +}; diff --git a/apps/web/src/components/loop/LogsPanel.tsx b/apps/web/src/components/loop/LogsPanel.tsx new file mode 100644 index 0000000..337b8e8 --- /dev/null +++ b/apps/web/src/components/loop/LogsPanel.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import { useAppStore } from "../../store/useAppStore"; +import { Card, CardContent, CardHeader, CardTitle } from "../ui/Card"; +import { Badge } from "../ui/Badge"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faCircleExclamation, + faCircleInfo, + faList, + faTriangleExclamation, +} from "@fortawesome/free-solid-svg-icons"; + +export const LogsPanel = () => { + const logs = useAppStore((s) => s.logs); + const selectedHash = useAppStore((s) => s.selectedHash); + const jobs = useAppStore((s) => s.jobs); + const job = jobs.find((j) => j.torrentHash === selectedHash); + const filtered = job ? logs.filter((log) => log.jobId === job.id) : logs; + + return ( + + + + + Logs + + Live + + +
+ {[...filtered].reverse().map((log, idx) => ( +
+
+ + + {log.level} + + {log.createdAt} +
+
{log.message}
+
+ ))} +
+
+
+ ); +}; diff --git a/apps/web/src/components/loop/LoopSetupCard.tsx b/apps/web/src/components/loop/LoopSetupCard.tsx new file mode 100644 index 0000000..b21fbd4 --- /dev/null +++ b/apps/web/src/components/loop/LoopSetupCard.tsx @@ -0,0 +1,111 @@ +import React, { useState } from "react"; +import { useAppStore } from "../../store/useAppStore"; +import { Card, CardContent, CardHeader, CardTitle } from "../ui/Card"; +import { Input } from "../ui/Input"; +import { Button } from "../ui/Button"; +import { api } from "../../api/client"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faCircleInfo, faClock, faList, faLock } from "@fortawesome/free-solid-svg-icons"; + +export const LoopSetupCard = () => { + const selectedHash = useAppStore((s) => s.selectedHash); + const jobs = useAppStore((s) => s.jobs); + const loopForm = useAppStore((s) => s.loopForm); + const setLoopForm = useAppStore((s) => s.setLoopForm); + const [dryRun, setDryRun] = useState(null); + + const job = jobs.find((j) => j.torrentHash === selectedHash); + + const startLoop = async () => { + if (!selectedHash) { + return; + } + await api.post("/api/loop/start", { + hash: selectedHash, + allowIp: loopForm.allowIp, + targetLoops: loopForm.targetLoops, + delayMs: loopForm.delayMs, + }); + }; + + const stopLoop = async () => { + if (!selectedHash) { + return; + } + await api.post("/api/loop/stop-by-hash", { hash: selectedHash }); + }; + + const runDry = async () => { + if (!selectedHash) { + return; + } + const response = await api.post("/api/loop/dry-run", { + hash: selectedHash, + allowIp: loopForm.allowIp || "127.0.0.1", + }); + setDryRun(JSON.stringify(response.data, null, 2)); + }; + + return ( + + + + + Loop Setup + + + +
+ + + +
+ + + +
+ {dryRun && ( +
+              {dryRun}
+            
+ )} +
+
+
+ ); +}; diff --git a/apps/web/src/components/loop/LoopStatsCard.tsx b/apps/web/src/components/loop/LoopStatsCard.tsx new file mode 100644 index 0000000..7f9496b --- /dev/null +++ b/apps/web/src/components/loop/LoopStatsCard.tsx @@ -0,0 +1,92 @@ +import React from "react"; +import { useAppStore } from "../../store/useAppStore"; +import { Card, CardContent, CardHeader, CardTitle } from "../ui/Card"; +import { Badge } from "../ui/Badge"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faBan, + faCircleInfo, + faClock, + faDownload, + faList, + faTriangleExclamation, +} from "@fortawesome/free-solid-svg-icons"; + +export const LoopStatsCard = () => { + const selectedHash = useAppStore((s) => s.selectedHash); + const jobs = useAppStore((s) => s.jobs); + const job = jobs.find((j) => j.torrentHash === selectedHash); + + const formatTotal = (bytes: number) => { + const gb = bytes / (1024 ** 3); + if (gb >= 1024) { + return `${(gb / 1024).toFixed(2)} TB`; + } + return `${gb.toFixed(2)} GB`; + }; + + if (!job) { + return ( + + + Loop Stats + + +

No active job for selection.

+
+
+ ); + } + + return ( + + + + + Loop Stats + + + {job.status} + + + +
+
+ + Loops: {job.doneLoops} / {job.targetLoops} +
+
+ + + Total Download:{" "} + {formatTotal( + job.totals?.totalDownloadedBytes ?? + job.doneLoops * job.sizeBytes + )} + +
+
+ + Delay: {job.delayMs} ms +
+
+ + Banned peers: {job.bans?.bannedIps?.length ?? 0} +
+ {job.nextRunAt && ( +
+ + Next run: {job.nextRunAt} +
+ )} + {job.lastError && ( +
+ + {job.lastError} +
+ )} +
+
+
+ ); +}; diff --git a/apps/web/src/components/loop/ProfilesCard.tsx b/apps/web/src/components/loop/ProfilesCard.tsx new file mode 100644 index 0000000..6a29bde --- /dev/null +++ b/apps/web/src/components/loop/ProfilesCard.tsx @@ -0,0 +1,185 @@ +import React, { useEffect, useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "../ui/Card"; +import { Input } from "../ui/Input"; +import { Button } from "../ui/Button"; +import { api } from "../../api/client"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faList, faLock, faUser } from "@fortawesome/free-solid-svg-icons"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "../ui/AlertDialog"; +import { useUiStore } from "../../store/useUiStore"; + +interface Profile { + id: string; + name: string; + allowIp: string; + delayMs: number; + targetLoops: number; +} + +export const ProfilesCard = ({ onApply }: { onApply?: (profile: Profile) => void }) => { + const [profiles, setProfiles] = useState([]); + const [name, setName] = useState(""); + const [allowIp, setAllowIp] = useState(""); + const [delayMs, setDelayMs] = useState(3000); + const [targetLoops, setTargetLoops] = useState(3); + const [editingId, setEditingId] = useState(null); + const pushAlert = useUiStore((s) => s.pushAlert); + + const loadProfiles = async () => { + const response = await api.get("/api/profiles"); + setProfiles(response.data); + }; + + useEffect(() => { + loadProfiles(); + }, []); + + const saveProfile = async () => { + const payload = { + name, + allowIp, + delayMs, + targetLoops, + }; + try { + if (editingId) { + const response = await api.put(`/api/profiles/${editingId}`, payload); + setProfiles((prev) => + prev.map((profile) => (profile.id === editingId ? response.data : profile)) + ); + pushAlert({ + title: "Profil güncellendi", + description: "Profil bilgileri kaydedildi.", + variant: "success", + }); + setEditingId(null); + } else { + const response = await api.post("/api/profiles", payload); + setProfiles((prev) => [...prev, response.data]); + pushAlert({ + title: "Profil kaydedildi", + description: "Yeni profil eklendi.", + variant: "success", + }); + } + setName(""); + setAllowIp(""); + setDelayMs(3000); + setTargetLoops(3); + } catch (error) { + pushAlert({ + title: "Profil kaydedilemedi", + description: "Lütfen bilgileri kontrol edip tekrar deneyin.", + variant: "error", + }); + } + }; + + const startEdit = (profile: Profile) => { + setEditingId(profile.id); + setName(profile.name); + setAllowIp(profile.allowIp); + setDelayMs(profile.delayMs); + setTargetLoops(profile.targetLoops); + }; + + const removeProfile = async (profileId: string) => { + try { + await api.delete(`/api/profiles/${profileId}`); + setProfiles((prev) => prev.filter((profile) => profile.id !== profileId)); + pushAlert({ + title: "Profil silindi", + description: "Profil listeden kaldırıldı.", + variant: "success", + }); + } catch (error) { + pushAlert({ + title: "Silme başarısız", + description: "Profil silinemedi.", + variant: "error", + }); + } + }; + + return ( + + + + + Profiles + + + +
+
+ setName(e.target.value)} /> + setAllowIp(e.target.value)} /> +
+ setDelayMs(Number(e.target.value))} /> + setTargetLoops(Number(e.target.value))} /> +
+ +
+
+ {profiles.map((profile) => ( +
+
+
{profile.name}
+
+ + + {profile.allowIp} + + + + {profile.targetLoops} loops + +
+
+
+ + + + + + + + + Profil silinsin mi? + + Bu işlem geri alınamaz. + + + + İptal + removeProfile(profile.id)}> + Sil + + + + +
+
+ ))} +
+
+
+
+ ); +}; diff --git a/apps/web/src/components/torrents/TorrentDetailsCard.tsx b/apps/web/src/components/torrents/TorrentDetailsCard.tsx new file mode 100644 index 0000000..7dcb7d5 --- /dev/null +++ b/apps/web/src/components/torrents/TorrentDetailsCard.tsx @@ -0,0 +1,111 @@ +import React, { useEffect, useState } from "react"; +import { useAppStore } from "../../store/useAppStore"; +import { Card, CardContent, CardHeader, CardTitle } from "../ui/Card"; +import { Badge } from "../ui/Badge"; +import { api } from "../../api/client"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faCircleInfo, + faDatabase, + faHashtag, + faLink, +} from "@fortawesome/free-solid-svg-icons"; + +export const TorrentDetailsCard = () => { + const selectedHash = useAppStore((s) => s.selectedHash); + const torrents = useAppStore((s) => s.torrents); + const [archiveStatus, setArchiveStatus] = useState("MISSING"); + + const torrent = torrents.find((t) => t.hash === selectedHash); + + const refreshArchiveStatus = async (hash: string) => { + try { + const response = await api.get(`/api/torrent/archive/status/${hash}`); + setArchiveStatus(response.data.status); + } catch (error) { + setArchiveStatus("MISSING"); + } + }; + + useEffect(() => { + if (!selectedHash) { + return; + } + api.post("/api/torrent/select", { hash: selectedHash }); + refreshArchiveStatus(selectedHash); + }, [selectedHash]); + + useEffect(() => { + const handler = (event: Event) => { + const detail = (event as CustomEvent<{ hash: string; status: string }>).detail; + if (detail?.hash === selectedHash && detail.status) { + setArchiveStatus(detail.status); + } + }; + window.addEventListener("archive-status", handler as EventListener); + return () => window.removeEventListener("archive-status", handler as EventListener); + }, [selectedHash]); + + if (!torrent) { + return ( + + + Selected Torrent + + +

Select a torrent to inspect.

+
+
+ ); + } + + return ( + + + + + Selected Torrent + + + Archive: {archiveStatus} + + + +
+
+ {torrent.name} +
+
+ + Hash: {torrent.hash} +
+
+ + + Size: {(torrent.size / (1024 * 1024 * 1024)).toFixed(2)} GB + +
+
+ + + Tracker:{" "} + {(() => { + if (!torrent.tracker) { + return "-"; + } + try { + const url = new URL(torrent.tracker); + return url.hostname; + } catch { + return torrent.tracker; + } + })()} + +
+
+
+
+ ); +}; diff --git a/apps/web/src/components/torrents/TorrentTable.tsx b/apps/web/src/components/torrents/TorrentTable.tsx new file mode 100644 index 0000000..21c8dda --- /dev/null +++ b/apps/web/src/components/torrents/TorrentTable.tsx @@ -0,0 +1,219 @@ +import React, { useMemo, useState } from "react"; +import { useAppStore } from "../../store/useAppStore"; +import { Card, CardContent, CardHeader, CardTitle } from "../ui/Card"; +import { Input } from "../ui/Input"; +import { api } from "../../api/client"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "../ui/AlertDialog"; +import { useUiStore } from "../../store/useUiStore"; + +const formatSpeed = (bytesPerSec: number) => { + const kb = bytesPerSec / 1024; + if (kb >= 1024) { + return `${(kb / 1024).toFixed(1)} MB/s`; + } + return `${Math.round(kb)} kB/s`; +}; + +export const TorrentTable = () => { + const torrents = useAppStore((s) => s.torrents); + const selected = useAppStore((s) => s.selectedHash); + const selectHash = useAppStore((s) => s.selectHash); + const pushAlert = useUiStore((s) => s.pushAlert); + const [query, setQuery] = useState(""); + + const filtered = useMemo(() => { + return torrents.filter((torrent) => + torrent.name.toLowerCase().includes(query.toLowerCase()) + ); + }, [torrents, query]); + + const deleteTorrent = async (hash: string) => { + try { + await api.delete(`/api/qbit/torrent/${hash}`); + pushAlert({ + title: "Torrent silindi", + description: "Torrent ve dosyalar diskten kaldırıldı.", + variant: "success", + }); + } catch (error) { + pushAlert({ + title: "Silme başarısız", + description: "Torrent silinemedi. Sunucu loglarını kontrol edin.", + variant: "error", + }); + } + }; + + const uploadTorrent = async (hash: string, file: File) => { + const form = new FormData(); + form.append("file", file); + form.append("hash", hash); + try { + const response = await api.post("/api/torrent/archive/upload", form, { + headers: { "Content-Type": "multipart/form-data" }, + }); + if (response.data?.added) { + pushAlert({ + title: "Yükleme başarılı", + description: "Torrent qBittorrent'a eklendi.", + variant: "success", + }); + } else { + pushAlert({ + title: "Yükleme yapıldı", + description: "Torrent arşive alındı, qBittorrent'a eklenemedi.", + variant: "warn", + }); + } + } catch (error: any) { + const apiError = error?.response?.data?.error; + pushAlert({ + title: "Yükleme başarısız", + description: apiError || "Sunucu loglarını kontrol edin.", + variant: "error", + }); + } + }; + + return ( + + + Torrents + setQuery(event.target.value)} + className="w-40" + /> + + +
+ {filtered.map((torrent) => ( +
+
selectHash(torrent.hash)} + > +
+ {torrent.name} +
+
+
+
+
+ {Math.round(torrent.progress * 100)}% + {formatSpeed(torrent.dlspeed)} + + {torrent.state} + +
+
+
+ + + + + + event.stopPropagation()}> + + Torrent silinsin mi? + + Bu işlem torrent ve indirilmiş dosyaları kalıcı olarak siler. + + + + İptal + deleteTorrent(torrent.hash)}> + Sil + + + + +
+
+ ))} +
+ + + ); +}; diff --git a/apps/web/src/components/ui/Alert.tsx b/apps/web/src/components/ui/Alert.tsx new file mode 100644 index 0000000..bcbe293 --- /dev/null +++ b/apps/web/src/components/ui/Alert.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import clsx from "clsx"; + +export const Alert = ({ + variant = "default", + className, + ...props +}: React.HTMLAttributes & { + variant?: "default" | "success" | "warn" | "error"; +}) => { + const variants: Record = { + default: "border-slate-200 bg-white text-slate-700", + success: "border-emerald-200 bg-emerald-50 text-emerald-800", + warn: "border-amber-200 bg-amber-50 text-amber-800", + error: "border-rose-200 bg-rose-50 text-rose-800", + }; + return ( +
+ ); +}; + +export const AlertTitle = ({ className, ...props }: React.HTMLAttributes) => ( +
+); + +export const AlertDescription = ({ className, ...props }: React.HTMLAttributes) => ( +

+); diff --git a/apps/web/src/components/ui/AlertDialog.tsx b/apps/web/src/components/ui/AlertDialog.tsx new file mode 100644 index 0000000..5e3b576 --- /dev/null +++ b/apps/web/src/components/ui/AlertDialog.tsx @@ -0,0 +1,69 @@ +import React from "react"; +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; +import clsx from "clsx"; + +export const AlertDialog = AlertDialogPrimitive.Root; +export const AlertDialogTrigger = AlertDialogPrimitive.Trigger; +export const AlertDialogPortal = AlertDialogPrimitive.Portal; + +export const AlertDialogOverlay = ({ className, ...props }: AlertDialogPrimitive.AlertDialogOverlayProps) => ( + +); + +export const AlertDialogContent = ({ + className, + ...props +}: AlertDialogPrimitive.AlertDialogContentProps) => ( + + + + +); + +export const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( +

+); + +export const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( +
+); + +export const AlertDialogTitle = ({ className, ...props }: AlertDialogPrimitive.AlertDialogTitleProps) => ( + +); + +export const AlertDialogDescription = ({ + className, + ...props +}: AlertDialogPrimitive.AlertDialogDescriptionProps) => ( + +); + +export const AlertDialogCancel = ({ className, ...props }: AlertDialogPrimitive.AlertDialogCancelProps) => ( + +); + +export const AlertDialogAction = ({ className, ...props }: AlertDialogPrimitive.AlertDialogActionProps) => ( + +); diff --git a/apps/web/src/components/ui/AlertToastStack.tsx b/apps/web/src/components/ui/AlertToastStack.tsx new file mode 100644 index 0000000..72c40c7 --- /dev/null +++ b/apps/web/src/components/ui/AlertToastStack.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import { Alert, AlertDescription, AlertTitle } from "./Alert"; +import { useUiStore } from "../../store/useUiStore"; + +export const AlertToastStack = () => { + const alerts = useUiStore((s) => s.alerts); + + if (alerts.length === 0) { + return null; + } + + return ( +
+ {alerts.map((alert) => ( + + {alert.title} + {alert.description ? ( + {alert.description} + ) : null} + + ))} +
+ ); +}; diff --git a/apps/web/src/components/ui/Badge.tsx b/apps/web/src/components/ui/Badge.tsx new file mode 100644 index 0000000..cf90f1b --- /dev/null +++ b/apps/web/src/components/ui/Badge.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import clsx from "clsx"; + +export const Badge = ({ + variant = "default", + className, + ...props +}: React.HTMLAttributes & { + variant?: "default" | "success" | "warn" | "danger"; +}) => { + const variants: Record = { + default: "bg-slate-100 text-slate-700", + success: "bg-emerald-100 text-emerald-700", + warn: "bg-amber-100 text-amber-700", + danger: "bg-rose-100 text-rose-700", + }; + return ( + + ); +}; diff --git a/apps/web/src/components/ui/Button.tsx b/apps/web/src/components/ui/Button.tsx new file mode 100644 index 0000000..f11e4f0 --- /dev/null +++ b/apps/web/src/components/ui/Button.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import clsx from "clsx"; + +export const Button = ({ + variant = "default", + className, + ...props +}: React.ButtonHTMLAttributes & { + variant?: "default" | "outline" | "ghost"; +}) => { + const base = + "inline-flex items-center justify-center rounded-md px-4 py-2 text-sm font-semibold transition"; + const variants: Record = { + default: "bg-ink text-white hover:bg-slate-800", + outline: "border border-slate-300 text-slate-700 hover:bg-slate-100", + ghost: "text-slate-600 hover:bg-slate-100", + }; + return ( + + + + +
+ ); +}; diff --git a/apps/web/src/pages/TimerPage.tsx b/apps/web/src/pages/TimerPage.tsx new file mode 100644 index 0000000..8626682 --- /dev/null +++ b/apps/web/src/pages/TimerPage.tsx @@ -0,0 +1,530 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/Card"; +import { Button } from "../components/ui/Button"; +import { Input } from "../components/ui/Input"; +import { api } from "../api/client"; +import { useAppStore } from "../store/useAppStore"; +import { useUiStore } from "../store/useUiStore"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "../components/ui/AlertDialog"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faClockRotateLeft, + faClock, + faTags, + faTrash, + faChartBar, + faPlus, + faHourglassHalf, +} from "@fortawesome/free-solid-svg-icons"; + +const unitOptions = [ + { label: "Saat", value: "hours", seconds: 3600 }, + { label: "Gün", value: "days", seconds: 86400 }, + { label: "Hafta", value: "weeks", seconds: 604800 }, +] as const; + +const formatBytes = (value: number) => { + if (!Number.isFinite(value)) return "0 B"; + const units = ["B", "KB", "MB", "GB", "TB"]; + let size = value; + let unitIndex = 0; + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex += 1; + } + return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unitIndex]}`; +}; + +const formatDuration = (seconds: number) => { + if (!Number.isFinite(seconds)) return "0 sn"; + if (seconds >= 86400) { + return `${(seconds / 86400).toFixed(1)} gün`; + } + if (seconds >= 3600) { + return `${(seconds / 3600).toFixed(1)} saat`; + } + return `${Math.max(1, Math.round(seconds / 60))} dk`; +}; + +const formatCountdown = (seconds: number) => { + if (!Number.isFinite(seconds)) return "—"; + const clamped = Math.max(0, Math.floor(seconds)); + const days = Math.floor(clamped / 86400); + const hours = Math.floor((clamped % 86400) / 3600); + const minutes = Math.floor((clamped % 3600) / 60); + const secs = clamped % 60; + const pad = (value: number) => value.toString().padStart(2, "0"); + return `${days}g ${pad(hours)}:${pad(minutes)}:${pad(secs)}`; +}; + +const trackerLabel = (tracker?: string) => { + if (!tracker) return "Bilinmiyor"; + try { + const host = new URL(tracker).hostname; + return host.replace(/^www\./, ""); + } catch { + return tracker; + } +}; + +export const TimerPage = () => { + const torrents = useAppStore((s) => s.torrents); + const timerRules = useAppStore((s) => s.timerRules); + const timerLogs = useAppStore((s) => s.timerLogs); + const timerSummary = useAppStore((s) => s.timerSummary); + const setTimerRules = useAppStore((s) => s.setTimerRules); + const setTimerLogs = useAppStore((s) => s.setTimerLogs); + const setTimerSummary = useAppStore((s) => s.setTimerSummary); + + const [selectedTags, setSelectedTags] = useState([]); + const [seedValue, setSeedValue] = useState(2); + const [seedUnit, setSeedUnit] = useState<(typeof unitOptions)[number]["value"]>( + "weeks" + ); + const [busy, setBusy] = useState(false); + const pushAlert = useUiStore((s) => s.pushAlert); + const [nowTick, setNowTick] = useState(() => Date.now()); + + const tagOptions = useMemo(() => { + const tags = new Set(); + torrents.forEach((torrent) => { + const tagList = torrent.tags + ? torrent.tags.split(",").map((tag) => tag.trim()) + : []; + tagList.filter(Boolean).forEach((tag) => tags.add(tag)); + if (torrent.category) { + tags.add(torrent.category); + } + }); + return Array.from(tags.values()).sort((a, b) => a.localeCompare(b)); + }, [torrents]); + + const rulesForDisplay = useMemo(() => { + return [...timerRules].sort((a, b) => b.createdAt.localeCompare(a.createdAt)); + }, [timerRules]); + + const matchingTorrents = useMemo(() => { + if (timerRules.length === 0) { + return []; + } + return torrents + .map((torrent) => { + const tags = (torrent.tags ?? "") + .split(",") + .map((tag) => tag.trim().toLowerCase()) + .filter(Boolean); + if (torrent.category) { + tags.push(torrent.category.toLowerCase()); + } + const addedOnMs = Number(torrent.added_on ?? 0) * 1000; + const matchingRules = timerRules.filter((rule) => { + return rule.tags.some((tag) => tags.includes(tag.toLowerCase())); + }); + if (matchingRules.length === 0) { + return null; + } + const rule = matchingRules.reduce((best, current) => + current.seedLimitSeconds < best.seedLimitSeconds ? current : best + ); + const ruleCreatedAtMs = Date.parse(rule.createdAt); + let baseMs = addedOnMs || nowTick; + if (Number.isFinite(ruleCreatedAtMs) && ruleCreatedAtMs > baseMs) { + baseMs = ruleCreatedAtMs; + } + const elapsedSeconds = Math.max(0, (nowTick - baseMs) / 1000); + const remainingSeconds = rule.seedLimitSeconds - elapsedSeconds; + return { + torrent, + rule, + remainingSeconds, + }; + }) + .filter(Boolean) as Array<{ + torrent: typeof torrents[number]; + rule: typeof timerRules[number]; + remainingSeconds: number; + }>; + }, [timerRules, torrents, nowTick]); + + useEffect(() => { + let active = true; + const load = async () => { + try { + const [rulesRes, logsRes, summaryRes] = await Promise.all([ + api.get("/api/timer/rules"), + api.get("/api/timer/logs"), + api.get("/api/timer/summary"), + ]); + if (!active) return; + setTimerRules(rulesRes.data ?? []); + setTimerLogs((logsRes.data ?? []).reverse()); + if (summaryRes.data) { + setTimerSummary(summaryRes.data); + } + } catch (err) { + if (active) { + pushAlert({ + title: "Timer verileri alınamadı", + description: "Bağlantıyı kontrol edip tekrar deneyin.", + variant: "error", + }); + } + } + }; + load(); + return () => { + active = false; + }; + }, [setTimerLogs, setTimerRules, setTimerSummary]); + + useEffect(() => { + const interval = setInterval(() => setNowTick(Date.now()), 1000); + return () => clearInterval(interval); + }, []); + + const toggleTag = (tag: string) => { + setSelectedTags((current) => + current.includes(tag) + ? current.filter((value) => value !== tag) + : [...current, tag] + ); + }; + + const handleSaveRule = async () => { + const unit = unitOptions.find((item) => item.value === seedUnit); + if (!unit) return; + if (selectedTags.length === 0) { + pushAlert({ + title: "Etiket seçilmedi", + description: "En az bir etiket seçmelisiniz.", + variant: "warn", + }); + return; + } + if (!Number.isFinite(seedValue) || seedValue <= 0) { + pushAlert({ + title: "Seed süresi geçersiz", + description: "Seed süresi 0’dan büyük olmalı.", + variant: "warn", + }); + return; + } + const seedLimitSeconds = Math.round(seedValue * unit.seconds); + setBusy(true); + try { + const response = await api.post("/api/timer/rules", { + tags: selectedTags, + seedLimitSeconds, + }); + setTimerRules([response.data, ...timerRules]); + setSelectedTags([]); + pushAlert({ + title: "Kural kaydedildi", + description: "Timer kuralı aktif edildi.", + variant: "success", + }); + } catch (err) { + pushAlert({ + title: "Kural kaydedilemedi", + description: "Lütfen daha sonra tekrar deneyin.", + variant: "error", + }); + } finally { + setBusy(false); + } + }; + + const handleDeleteRule = async (ruleId: string) => { + try { + await api.delete(`/api/timer/rules/${ruleId}`); + setTimerRules(timerRules.filter((rule) => rule.id !== ruleId)); + pushAlert({ + title: "Kural silindi", + description: "Timer kuralı kaldırıldı.", + variant: "success", + }); + } catch (err) { + pushAlert({ + title: "Kural silinemedi", + description: "Lütfen daha sonra tekrar deneyin.", + variant: "error", + }); + } + }; + + const summary = timerSummary ?? { + totalDeleted: 0, + totalSeededSeconds: 0, + totalUploadedBytes: 0, + updatedAt: "", + }; + + return ( +
+
+ + + + + Zamanlayıcı Torrentleri + + + + {matchingTorrents.length === 0 ? ( +
+ Bu kurallara bağlı aktif torrent bulunamadı. +
+ ) : ( +
+ {matchingTorrents.map(({ torrent, rule, remainingSeconds }) => ( +
+
+
+
+ {torrent.name} +
+
+ {formatBytes(torrent.size)} • {trackerLabel(torrent.tracker)} +
+
+
+
+ {formatCountdown(remainingSeconds)} +
+
Kural: {formatDuration(rule.seedLimitSeconds)}
+
+
+
+ Hash: {torrent.hash.slice(0, 12)}... + + Etiket:{" "} + {(torrent.tags || torrent.category || "-") + .split(",") + .map((tag) => tag.trim()) + .filter(Boolean) + .join(", ") || "-"} + +
+
+ ))} +
+ )} +
+
+ + + + + Silinen Torrent Logları + + + + {timerLogs.length === 0 ? ( +
Henüz log yok.
+ ) : ( +
+ {timerLogs.map((log) => ( +
+
+
+
+ {log.name} +
+
+ {formatBytes(log.sizeBytes)} •{" "} + {trackerLabel(log.tracker)} +
+
+
+ {new Date(log.deletedAt).toLocaleString()} +
+
+
+ Seed: {formatDuration(log.seedingTimeSeconds)} + Upload: {formatBytes(log.uploadedBytes)} + {log.tags?.length ? ( + Tags: {log.tags.join(", ")} + ) : null} +
+
+ ))} +
+ )} +
+
+
+ +
+ + + + + Timer Kuralı Oluştur + + + +
+
+ + Etiketler +
+
+ {tagOptions.length === 0 ? ( +
+ Henüz etiket bulunamadı. +
+ ) : ( + tagOptions.map((tag) => ( + + )) + )} +
+
+
+
+ + Seed Süresi +
+
+ setSeedValue(Number(event.target.value))} + /> + +
+
+ +
+
+ + Eklenen Kurallar +
+ {rulesForDisplay.length === 0 ? ( +
+ Henüz kural yok. +
+ ) : ( +
+ {rulesForDisplay.map((rule) => ( +
+
+
+ {rule.tags.join(", ")} +
+
+ Seed limiti: {formatDuration(rule.seedLimitSeconds)} +
+
+ + + + + + + Kural silinsin mi? + + Bu kural kaldırılınca zamanlayıcı artık bu etiketleri izlemeyecek. + + + + İptal + handleDeleteRule(rule.id)}> + Sil + + + + +
+ ))} +
+ )} +
+
+
+ + + + + Timer Özeti + + + +
+ Silinen dosya sayısı + + {summary.totalDeleted} + +
+
+ Toplam seed süresi + + {formatDuration(summary.totalSeededSeconds)} + +
+
+ Toplam upload + + {formatBytes(summary.totalUploadedBytes)} + +
+
+
+
+
+ ); +}; diff --git a/apps/web/src/socket/socket.ts b/apps/web/src/socket/socket.ts new file mode 100644 index 0000000..58b58ef --- /dev/null +++ b/apps/web/src/socket/socket.ts @@ -0,0 +1,67 @@ +import { io } from "socket.io-client"; +import { useAppStore } from "../store/useAppStore"; +import { api } from "../api/client"; + +let socket: ReturnType | null = null; + +export const connectSocket = () => { + if (socket) { + return socket; + } + const baseUrl = import.meta.env.VITE_API_BASE || undefined; + socket = io(baseUrl, { + withCredentials: true, + autoConnect: false, + }); + + api + .get("/api/auth/socket-token") + .then((response) => { + socket!.auth = { token: response.data.token }; + socket!.connect(); + }) + .catch(() => { + socket!.connect(); + }); + + socket.on("status:snapshot", (snapshot) => { + useAppStore.getState().setSnapshot(snapshot); + }); + + socket.on("status:update", (snapshot) => { + useAppStore.getState().updateStatus(snapshot); + }); + + socket.on("job:metrics", (job) => { + const state = useAppStore.getState(); + const jobs = state.jobs.map((existing) => + existing.id === job.id ? job : existing + ); + state.updateStatus({ jobs }); + }); + + socket.on("job:log", (log) => { + useAppStore.getState().addLog(log); + }); + + socket.on("qbit:health", (qbit) => { + useAppStore.getState().updateStatus({ qbit }); + }); + + socket.on("timer:log", (log) => { + useAppStore.getState().addTimerLog(log); + }); + + socket.on("timer:summary", (summary) => { + useAppStore.getState().setTimerSummary(summary); + }); + + return socket; +}; + +export const disconnectSocket = () => { + if (socket) { + socket.disconnect(); + socket = null; + } +}; diff --git a/apps/web/src/store/useAppStore.ts b/apps/web/src/store/useAppStore.ts new file mode 100644 index 0000000..896a471 --- /dev/null +++ b/apps/web/src/store/useAppStore.ts @@ -0,0 +1,140 @@ +import { create } from "zustand"; + +export interface TorrentInfo { + hash: string; + name: string; + size: number; + progress: number; + dlspeed: number; + state: string; + magnet_uri?: string; + tracker?: string; + tags?: string; + category?: string; + added_on?: number; + seeding_time?: number; +} + +export interface LoopJob { + id: string; + torrentHash: string; + name: string; + sizeBytes: number; + allowIp: string; + targetLoops: number; + doneLoops: number; + delayMs: number; + status: string; + bans: { bannedIps: string[] }; + nextRunAt?: string; + lastError?: string; + totals?: { totalDownloadedBytes: number }; +} + +export interface StatusSnapshot { + qbit: { ok: boolean; version?: string; lastError?: string }; + torrents: TorrentInfo[]; + transfer?: any; + jobs: LoopJob[]; +} + +export interface JobLog { + jobId: string; + level: "INFO" | "WARN" | "ERROR"; + message: string; + createdAt: string; +} + +export interface TimerRule { + id: string; + tags: string[]; + seedLimitSeconds: number; + createdAt: string; +} + +export interface TimerLog { + id: string; + hash: string; + name: string; + sizeBytes: number; + tracker?: string; + tags: string[]; + category?: string; + seedingTimeSeconds: number; + uploadedBytes: number; + deletedAt: string; +} + +export interface TimerSummary { + totalDeleted: number; + totalSeededSeconds: number; + totalUploadedBytes: number; + updatedAt: string; +} + +interface AppState { + qbit: StatusSnapshot["qbit"]; + torrents: TorrentInfo[]; + transfer: any; + jobs: LoopJob[]; + logs: JobLog[]; + timerRules: TimerRule[]; + timerLogs: TimerLog[]; + timerSummary: TimerSummary | null; + selectedHash: string | null; + loopForm: { allowIp: string; delayMs: number; targetLoops: number }; + setSnapshot: (snapshot: StatusSnapshot) => void; + updateStatus: (snapshot: Partial) => void; + addLog: (log: JobLog) => void; + setTimerRules: (rules: TimerRule[]) => void; + setTimerLogs: (logs: TimerLog[]) => void; + addTimerLog: (log: TimerLog) => void; + setTimerSummary: (summary: TimerSummary) => void; + selectHash: (hash: string) => void; + setLoopForm: (partial: Partial) => void; +} + +export const useAppStore = create((set) => ({ + qbit: { ok: false }, + torrents: [], + transfer: null, + jobs: [], + logs: [], + timerRules: [], + timerLogs: [], + timerSummary: null, + selectedHash: null, + loopForm: { allowIp: "", delayMs: 3000, targetLoops: 3 }, + setSnapshot: (snapshot) => + set((state) => ({ + qbit: snapshot.qbit, + torrents: snapshot.torrents, + transfer: snapshot.transfer, + jobs: snapshot.jobs, + selectedHash: + state.selectedHash ?? snapshot.torrents?.[0]?.hash ?? null, + })), + updateStatus: (snapshot) => + set((state) => ({ + qbit: snapshot.qbit ?? state.qbit, + torrents: snapshot.torrents ?? state.torrents, + transfer: snapshot.transfer ?? state.transfer, + jobs: snapshot.jobs ?? state.jobs, + })), + addLog: (log) => + set((state) => ({ + logs: [...state.logs, log].slice(-500), + })), + setTimerRules: (rules) => set({ timerRules: rules }), + setTimerLogs: (logs) => set({ timerLogs: logs }), + addTimerLog: (log) => + set((state) => ({ + timerLogs: [log, ...state.timerLogs].slice(0, 500), + })), + setTimerSummary: (summary) => set({ timerSummary: summary }), + selectHash: (hash) => set({ selectedHash: hash }), + setLoopForm: (partial) => + set((state) => ({ + loopForm: { ...state.loopForm, ...partial }, + })), +})); diff --git a/apps/web/src/store/useAuthStore.ts b/apps/web/src/store/useAuthStore.ts new file mode 100644 index 0000000..696f4e8 --- /dev/null +++ b/apps/web/src/store/useAuthStore.ts @@ -0,0 +1,40 @@ +import { create } from "zustand"; +import { api } from "../api/client"; + +interface AuthState { + username: string | null; + loading: boolean; + error: string | null; + login: (username: string, password: string) => Promise; + logout: () => Promise; + check: () => Promise; +} + +export const useAuthStore = create((set) => ({ + username: null, + loading: false, + error: null, + login: async (username, password) => { + set({ loading: true, error: null }); + try { + const response = await api.post("/api/auth/login", { username, password }); + set({ username: response.data.username, loading: false }); + return true; + } catch (error) { + set({ error: "Login failed", loading: false }); + return false; + } + }, + logout: async () => { + await api.post("/api/auth/logout"); + set({ username: null }); + }, + check: async () => { + try { + const response = await api.get("/api/auth/me"); + set({ username: response.data.username ?? "session" }); + } catch (error) { + set({ username: null }); + } + }, +})); diff --git a/apps/web/src/store/useUiStore.ts b/apps/web/src/store/useUiStore.ts new file mode 100644 index 0000000..350a4d7 --- /dev/null +++ b/apps/web/src/store/useUiStore.ts @@ -0,0 +1,31 @@ +import { create } from "zustand"; + +export type AlertVariant = "default" | "success" | "warn" | "error"; + +export interface UiAlert { + id: string; + title: string; + description?: string; + variant: AlertVariant; +} + +interface UiState { + alerts: UiAlert[]; + pushAlert: (alert: Omit) => void; + removeAlert: (id: string) => void; +} + +const generateId = () => Math.random().toString(36).slice(2); + +export const useUiStore = create((set, get) => ({ + alerts: [], + pushAlert: (alert) => { + const id = generateId(); + set((state) => ({ alerts: [{ ...alert, id }, ...state.alerts].slice(0, 5) })); + setTimeout(() => { + get().removeAlert(id); + }, 3000); + }, + removeAlert: (id) => + set((state) => ({ alerts: state.alerts.filter((item) => item.id !== id) })), +})); diff --git a/apps/web/tailwind.config.cjs b/apps/web/tailwind.config.cjs new file mode 100644 index 0000000..5616b1c --- /dev/null +++ b/apps/web/tailwind.config.cjs @@ -0,0 +1,14 @@ +module.exports = { + content: ["./src/**/*.{ts,tsx}", "./index.html"], + theme: { + extend: { + colors: { + ink: "#0f172a", + fog: "#e2e8f0", + mint: "#14b8a6", + steel: "#94a3b8" + } + } + }, + plugins: [] +}; diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..a88bd00 --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "strict": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts new file mode 100644 index 0000000..f2a0e3c --- /dev/null +++ b/apps/web/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import path from "node:path"; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: path.resolve(__dirname, "../server/public"), + emptyOutDir: true, + }, + server: { + host: "0.0.0.0", + port: Number(process.env.WEB_PORT) || 5173, + }, +}); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..efbfa8f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +version: "3.9" +services: + server: + build: + context: . + dockerfile: Dockerfile + ports: + - "${SERVER_PORT:-3001}:3001" + volumes: + - ./data:/app/data + environment: + - NODE_ENV=production + - QBIT_BASE_URL=${QBIT_BASE_URL} + - QBIT_USERNAME=${QBIT_USERNAME} + - QBIT_PASSWORD=${QBIT_PASSWORD} + - APP_USERNAME=${APP_USERNAME} + - APP_PASSWORD=${APP_PASSWORD} + - JWT_SECRET=${JWT_SECRET} + - POLL_INTERVAL_MS=${POLL_INTERVAL_MS} + - ENFORCE_INTERVAL_MS=${ENFORCE_INTERVAL_MS} + - DEFAULT_DELAY_MS=${DEFAULT_DELAY_MS} + - MAX_LOOP_LIMIT=${MAX_LOOP_LIMIT} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2252d1b --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "q-buffer", + "private": true, + "packageManager": "pnpm@9.0.0", + "workspaces": [ + "apps/*" + ], + "scripts": { + "dev": "pnpm -C apps/server dev & pnpm -C apps/web dev --host 0.0.0.0", + "build": "pnpm -C apps/server build && pnpm -C apps/web build", + "start": "pnpm -C apps/server start" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..17f0f52 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,5316 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: {} + + apps/server: + dependencies: + axios: + specifier: ^1.7.7 + version: 1.13.2 + axios-cookiejar-support: + specifier: ^5.0.2 + version: 5.0.5(axios@1.13.2)(tough-cookie@4.1.4) + bcryptjs: + specifier: ^2.4.3 + version: 2.4.3 + cookie-parser: + specifier: ^1.4.6 + version: 1.4.7 + cors: + specifier: ^2.8.5 + version: 2.8.5 + express: + specifier: ^4.19.2 + version: 4.22.1 + express-rate-limit: + specifier: ^7.3.1 + version: 7.5.1(express@4.22.1) + form-data: + specifier: ^4.0.0 + version: 4.0.5 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.3 + multer: + specifier: ^1.4.5-lts.1 + version: 1.4.5-lts.2 + parse-torrent: + specifier: ^9.1.5 + version: 9.1.5 + pino: + specifier: ^9.3.2 + version: 9.14.0 + socket.io: + specifier: ^4.7.5 + version: 4.8.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) + tough-cookie: + specifier: ^4.1.4 + version: 4.1.4 + webtorrent: + specifier: ^2.4.5 + version: 2.8.5(bare-url@2.3.2) + zod: + specifier: ^3.23.8 + version: 3.25.76 + devDependencies: + '@types/bcryptjs': + specifier: ^2.4.6 + version: 2.4.6 + '@types/cookie-parser': + specifier: ^1.4.7 + version: 1.4.10(@types/express@4.17.25) + '@types/cors': + specifier: ^2.8.17 + version: 2.8.19 + '@types/express': + specifier: ^4.17.21 + version: 4.17.25 + '@types/jsonwebtoken': + specifier: ^9.0.6 + version: 9.0.10 + '@types/multer': + specifier: ^1.4.12 + version: 1.4.13 + '@types/node': + specifier: ^20.14.9 + version: 20.19.27 + ts-node-dev: + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.19.27)(typescript@5.9.3) + typescript: + specifier: ^5.5.3 + version: 5.9.3 + + apps/web: + dependencies: + '@fortawesome/fontawesome-svg-core': + specifier: ^6.6.0 + version: 6.7.2 + '@fortawesome/free-solid-svg-icons': + specifier: ^6.6.0 + version: 6.7.2 + '@fortawesome/react-fontawesome': + specifier: ^0.2.2 + version: 0.2.6(@fortawesome/fontawesome-svg-core@6.7.2)(react@18.3.1) + '@radix-ui/react-alert-dialog': + specifier: ^1.1.2 + version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + axios: + specifier: ^1.7.7 + version: 1.13.2 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + react-router-dom: + specifier: ^6.26.2 + version: 6.30.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + socket.io-client: + specifier: ^4.7.5 + version: 4.8.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) + zustand: + specifier: ^4.5.5 + version: 4.5.7(@types/react@18.3.27)(react@18.3.1) + devDependencies: + '@types/react': + specifier: ^18.3.3 + version: 18.3.27 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.7(@types/react@18.3.27) + '@vitejs/plugin-react': + specifier: ^4.3.1 + version: 4.7.0(vite@5.4.21(@types/node@20.19.27)) + autoprefixer: + specifier: ^10.4.19 + version: 10.4.23(postcss@8.5.6) + postcss: + specifier: ^8.4.39 + version: 8.5.6 + tailwindcss: + specifier: ^3.4.6 + version: 3.4.19 + typescript: + specifier: ^5.5.3 + version: 5.9.3 + vite: + specifier: ^5.3.3 + version: 5.4.21(@types/node@20.19.27) + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.5': + resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.5': + resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.5': + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@fortawesome/fontawesome-common-types@6.7.2': + resolution: {integrity: sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==} + engines: {node: '>=6'} + + '@fortawesome/fontawesome-svg-core@6.7.2': + resolution: {integrity: sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==} + engines: {node: '>=6'} + + '@fortawesome/free-solid-svg-icons@6.7.2': + resolution: {integrity: sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==} + engines: {node: '>=6'} + + '@fortawesome/react-fontawesome@0.2.6': + resolution: {integrity: sha512-mtBFIi1UsYQo7rYonYFkjgYKGoL8T+fEH6NGUpvuqtY3ytMsAoDaPo5rk25KuMtKDipY4bGYM/CkmCHA1N3FUg==} + deprecated: v0.2.x is no longer supported. Unless you are still using FontAwesome 5, please update to v3.1.1 or greater. + peerDependencies: + '@fortawesome/fontawesome-svg-core': ~1 || ~6 || ~7 + react: ^16.3 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-alert-dialog@1.1.15': + resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@remix-run/router@1.23.1': + resolution: {integrity: sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==} + engines: {node: '>=14.0.0'} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.54.0': + resolution: {integrity: sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.54.0': + resolution: {integrity: sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.54.0': + resolution: {integrity: sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.54.0': + resolution: {integrity: sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.54.0': + resolution: {integrity: sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.54.0': + resolution: {integrity: sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.54.0': + resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.54.0': + resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.54.0': + resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.54.0': + resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.54.0': + resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.54.0': + resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.54.0': + resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.54.0': + resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.54.0': + resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.54.0': + resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.54.0': + resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.54.0': + resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.54.0': + resolution: {integrity: sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.54.0': + resolution: {integrity: sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.54.0': + resolution: {integrity: sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.54.0': + resolution: {integrity: sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==} + cpu: [x64] + os: [win32] + + '@silentbot1/nat-api@0.4.9': + resolution: {integrity: sha512-Bm2Fr0sJyGr4B/XgKjQxjGe7Rzs/OlK91OIHsghObxhP3Y4j2y8o7Xjlledu/pxzFEIWaTbZIBSl8ABqoP/WhQ==} + engines: {node: '>=10.0.0'} + + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + + '@thaunknown/simple-peer@10.0.12': + resolution: {integrity: sha512-sDrkkOdzlJL8+FXQqYcBb2THHQU+Yrar92SjfW4ZLs877/4QA2kFejuA6DVepsoMpoIbXShc7OCXCwYt4AtGdQ==} + + '@thaunknown/simple-websocket@9.1.3': + resolution: {integrity: sha512-pf/FCJsgWtLJiJmIpiSI7acOZVq3bIQCpnNo222UFc8Ph1lOUOTpe6LoYhhiOSKB9GUaWJEVUtZ+sK1/aBgU5Q==} + + '@thaunknown/thirty-two@1.0.5': + resolution: {integrity: sha512-Q53KyCXweV1CS62EfqtPDqfpksn5keQ59PGqzzkK+g8Vif1jB4inoBCcs/BUSdsqddhE3G+2Fn+4RX3S6RqT0A==} + engines: {node: '>=0.2.6'} + + '@tsconfig/node10@1.0.12': + resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/bcryptjs@2.4.6': + resolution: {integrity: sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cookie-parser@1.4.10': + resolution: {integrity: sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==} + peerDependencies: + '@types/express': '*' + + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/express-serve-static-core@4.19.7': + resolution: {integrity: sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==} + + '@types/express@4.17.25': + resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/multer@1.4.13': + resolution: {integrity: sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==} + + '@types/node@20.19.27': + resolution: {integrity: sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.27': + resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==} + + '@types/send@0.17.6': + resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@1.15.10': + resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + + '@types/strip-bom@3.0.0': + resolution: {integrity: sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==} + + '@types/strip-json-comments@0.0.30': + resolution: {integrity: sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@webtorrent/http-node@1.3.0': + resolution: {integrity: sha512-GWZQKroPES4z91Ijx6zsOsb7+USOxjy66s8AoTWg0HiBBdfnbtf9aeh3Uav0MgYn4BL8Q7tVSUpd0gGpngKGEQ==} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + addr-to-ip-port@2.0.0: + resolution: {integrity: sha512-9bYbtjamtdLHZSqVIUXhilOryNPiL+x+Q5J/Unpg4VY3ZIkK3fT52UoErj1NdUeVm3J1t2iBEAur4Ywbl/bahw==} + engines: {node: '>=12.20.0'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + autoprefixer@10.4.23: + resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + axios-cookiejar-support@5.0.5: + resolution: {integrity: sha512-jJG+p7JnOYxkVrYkCDKBrLqUmcpwHZTNQrEcIEKr5qe7YVTyPAD9nCsi1cO5LDmQpQApfS430czO+oceI3g/3g==} + engines: {node: '>=18.0.0'} + peerDependencies: + axios: '>=0.20.0' + tough-cookie: '>=4.0.0' + + axios@1.13.2: + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + + b4a@1.7.3: + resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + bare-addon-resolve@1.9.6: + resolution: {integrity: sha512-hvOQY1zDK6u0rSr27T6QlULoVLwi8J2k8HHHJlxSfT7XQdQ/7bsS+AnjYkHtu/TkL+gm3aMXAKucJkJAbrDG/g==} + peerDependencies: + bare-url: '*' + peerDependenciesMeta: + bare-url: + optional: true + + bare-events@2.8.2: + resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + bare-fs@4.5.2: + resolution: {integrity: sha512-veTnRzkb6aPHOvSKIOy60KzURfBdUflr5VReI+NSaPL6xf+XLdONQgZgpYvUuZLVQ8dCqxpBAudaOM1+KpAUxw==} + engines: {bare: '>=1.16.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-module-resolve@1.12.0: + resolution: {integrity: sha512-JrzrqlC3Tds0iKRwQs8xIIJ+FRieKA9ll0jaqpotDLZtjJPVevzRoeuUYZ5GIo1t1z7/pIRdk85Q3i/2xQLfEQ==} + peerDependencies: + bare-url: '*' + peerDependenciesMeta: + bare-url: + optional: true + + bare-os@3.6.2: + resolution: {integrity: sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==} + engines: {bare: '>=1.14.0'} + + bare-path@3.0.0: + resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} + + bare-semver@1.0.2: + resolution: {integrity: sha512-ESVaN2nzWhcI5tf3Zzcq9aqCZ676VWzqw07eEZ0qxAcEOAFYBa0pWq8sK34OQeHLY3JsfKXZS9mDyzyxGjeLzA==} + + bare-stream@2.7.0: + resolution: {integrity: sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==} + peerDependencies: + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.3.2: + resolution: {integrity: sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==} + + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + + baseline-browser-mapping@2.9.11: + resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} + hasBin: true + + bcryptjs@2.4.3: + resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==} + + bencode@2.0.3: + resolution: {integrity: sha512-D/vrAD4dLVX23NalHwb8dSvsUsxeRPO8Y7ToKA015JQYq69MLDOMkC0uGZYA/MPpltLO8rt8eqFC2j8DxjTZ/w==} + + bencode@4.0.0: + resolution: {integrity: sha512-AERXw18df0pF3ziGOCyUjqKZBVNH8HV3lBxnx5w0qtgMIk4a1wb9BkcCQbkp9Zstfrn/dzRwl7MmUHHocX3sRQ==} + engines: {node: '>=12.20.0'} + + bep53-range@1.1.1: + resolution: {integrity: sha512-ct6s33iiwRCUPp9KXnJ4QMWDgHIgaw36caK/5XEQ9L8dCzSQlJt1Vk6VmHh1VD4AlGCAI4C2zmtfItifBBPrhQ==} + + bep53-range@2.0.0: + resolution: {integrity: sha512-sMm2sV5PRs0YOVk0LTKtjuIprVzxgTQUsrGX/7Yph2Rm4FO2Fqqtq7hNjsOB5xezM4v4+5rljCgK++UeQJZguA==} + engines: {node: '>=12.20.0'} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bitfield@4.2.0: + resolution: {integrity: sha512-kUTatQb/mBd8uhvdLrUkouGDBUQiJaIOvPlptUwOWp6MFqih4d1MiVf0m3ATxfZSzu+LjW/awFeABltYa62uIA==} + engines: {node: '>=8'} + + bittorrent-dht@11.0.11: + resolution: {integrity: sha512-5rWMoK/2XjcPSx9nfqiL6mHxsXLwohX+81aL4a3qUyGU1992mBqARQE/evZ+a6eWb5DeRjeDU+qFZm11rmPZnQ==} + engines: {node: '>=12.20.0'} + + bittorrent-lsd@2.0.0: + resolution: {integrity: sha512-jV+SMTGNY1iGWjf5cPA2HMeA6axuMQRWwWELtsuZ1FmQmZwC74we92nwtDTfv1WMnLx+oqEjWRri42IHjZitSQ==} + engines: {node: '>=12.20.0'} + + bittorrent-peerid@1.3.6: + resolution: {integrity: sha512-VyLcUjVMEOdSpHaCG/7odvCdLbAB1y3l9A2V6WIje24uV7FkJPrQrH/RrlFmKxP89pFVDEnE+YlHaFujlFIZsg==} + + bittorrent-protocol@4.1.21: + resolution: {integrity: sha512-CcuPt6BL7gXa8BF+0GckYcQmr44ARfSPM0rYwMeYgWg+jftekWgy5vuxX6wJDpC5bKFvqNG+74bPBjyM7Swxrw==} + engines: {node: '>=12.20.0'} + + bittorrent-tracker@11.2.2: + resolution: {integrity: sha512-pVjpd6tPtLByrYwtDOo+cVx9zQZ2XUvlaWrlm57+9yvVDKHuNL+TFEAtyfXuIutghG7Bde/uWXGfoVWpPYY+8A==} + engines: {node: '>=16.0.0'} + hasBin: true + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + blob-to-buffer@1.2.9: + resolution: {integrity: sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA==} + + block-iterator@1.1.1: + resolution: {integrity: sha512-DrjdVWZemVO4iBf4tiOXjUrY5cNesjzy0t7sIiu2rdl8cOCHRxAgKjSJFc3vBZYYMMmshUAxajl8QQh/uxXTKQ==} + + body-parser@1.20.4: + resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + bufferutil@4.1.0: + resolution: {integrity: sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==} + engines: {node: '>=6.14.2'} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + cache-chunk-store@3.2.2: + resolution: {integrity: sha512-2lJdWbgHFFxcSth9s2wpId3CR3v1YC63KjP4T9WhpW7LWlY7Hiiei3QwwqzkWqlJTfR8lSy9F5kRQECeyj+yQA==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001762: + resolution: {integrity: sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + chrome-dgram@3.0.6: + resolution: {integrity: sha512-bqBsUuaOiXiqxXt/zA/jukNJJ4oaOtc7ciwqJpZVEaaXwwxqgI2/ZdG02vXYWUhHGziDlvGMQWk0qObgJwVYKA==} + + chrome-dns@1.0.1: + resolution: {integrity: sha512-HqsYJgIc8ljJJOqOzLphjAs79EUuWSX3nzZi2LNkzlw3GIzAeZbaSektC8iT/tKvLqZq8yl1GJu5o6doA4TRbg==} + + chrome-net@3.3.4: + resolution: {integrity: sha512-Jzy2EnzmE+ligqIZUsmWnck9RBXLuUy6CaKyuNMtowFG3ZvLt8d+WBJCTPEludV0DHpIKjAOlwjFmTaEdfdWCw==} + + chunk-store-iterator@1.0.4: + resolution: {integrity: sha512-LGjzJNmk7W1mrdaBoJNztPumT2ACmgjHmI1AMm8aeGYOl4+LKaYC/yfnx27i++LiAtoe/dR+3jC8HRzb6gW4/A==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + compact2string@1.4.1: + resolution: {integrity: sha512-3D+EY5nsRhqnOwDxveBv5T8wGo4DEvYxjDtPGmdOX+gfr5gE92c2RC0w2wa+xEefm07QuVqqcF3nZJUZ92l/og==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concat-stream@1.6.2: + resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} + engines: {'0': node >= 0.8} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-parser@1.4.7: + resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==} + engines: {node: '>= 0.8.0'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + cpus@1.0.3: + resolution: {integrity: sha512-PXHBvGLuL69u55IkLa5e5838fLhIMHxmkV4ge42a8alGyn7BtawYgI0hQ849EedvtHIOLNNH3i6eQU1BiE9SUA==} + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + create-torrent@6.1.0: + resolution: {integrity: sha512-War593HCsg4TotHgMGWTJqnDHN0pmEU2RM13xUzzSZ78TpRNOC2bbcsC5yMO3pqIkedHEWFzYNqH1yhwuuBYTg==} + engines: {node: '>=12'} + hasBin: true + + cross-fetch-ponyfill@1.0.3: + resolution: {integrity: sha512-uOBkDhUAGAbx/FEzNKkOfx3w57H8xReBBXoZvUnOKTI0FW0Xvrj3GrYv2iZXUqlffC1LMGfQzhmBM/ke+6eTDA==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + default-gateway@7.2.2: + resolution: {integrity: sha512-AD7TrdNNPXRZIGw63dw+lnGmT4v7ggZC5NHNJgAYWm5njrwoze1q5JSAW9YuLy2tjnoLUG/r8FEB93MCh9QJPg==} + engines: {node: '>= 16'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + dynamic-dedupe@0.3.0: + resolution: {integrity: sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.5.267: + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + engine.io-client@6.6.4: + resolution: {integrity: sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + engine.io@6.6.5: + resolution: {integrity: sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==} + engines: {node: '>=10.2.0'} + + err-code@3.0.1: + resolution: {integrity: sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + execa@7.2.0: + resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==} + engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@4.22.1: + resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} + engines: {node: '>= 0.10.0'} + + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-readable-async-iterator@2.0.0: + resolution: {integrity: sha512-8Sld+DuyWRIftl86ZguJxR2oXCBccOiJxrY/Rj9/7ZBynW8pYMWzIcqxFL1da+25jaWJZVa+HHX/8SsA21JdTA==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + filename-reserved-regex@3.0.0: + resolution: {integrity: sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@1.3.2: + resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} + engines: {node: '>= 0.8'} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + freelist@1.0.3: + resolution: {integrity: sha512-Ji7fEnMdZDGbS5oXElpRJsn9jPvBR8h/037D3bzreNmS8809cISq/2D9//JbA/TaZmkkN8cmecXwmQHmM+NHhg==} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fs-chunk-store@5.0.0: + resolution: {integrity: sha512-tKlT0joU9KmsLn0dTbVYVUa7VNqYQhl0X2qPPsN9lPEc3guXOmQJWY5/7kpo34Sk273qyWT5mqEhROCQPF+JKw==} + engines: {node: '>=12.20.0'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-native-extensions@1.4.5: + resolution: {integrity: sha512-ekV0T//iDm4AvhOcuPaHpxub4DI7HvY5ucLJVDvi7T2J+NZkQ9S6MuvgP0yeQvoqNUaAGyLjVYb1905BF9bpmg==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsa-chunk-store@1.3.0: + resolution: {integrity: sha512-0WCfuxqqSB6Tz/g7Ar/nwAxMoigXaIXuvfrnLIEFYIA9uc6w9eNaHuBGzU1X3lyM4cpLKCOTUmKAA/gCiTvzMQ==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stdin@8.0.0: + resolution: {integrity: sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==} + engines: {node: '>=10'} + + get-stdin@9.0.0: + resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} + engines: {node: '>=12'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + http-cookie-agent@6.0.8: + resolution: {integrity: sha512-qnYh3yLSr2jBsTYkw11elq+T361uKAJaZ2dR4cfYZChw1dt9uL5t3zSUwehoqqVb4oldk1BpkXKm2oat8zV+oA==} + engines: {node: '>=18.0.0'} + peerDependencies: + tough-cookie: ^4.0.0 || ^5.0.0 + undici: ^5.11.0 || ^6.0.0 + peerDependenciesMeta: + undici: + optional: true + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http-parser-js@0.4.13: + resolution: {integrity: sha512-u8u5ZaG0Tr/VvHlucK2ufMuOp4/5bvwgneXle+y228K5rMbJOlVjThONcaAw3ikAy8b2OO9RfEucdMHFz3UWMA==} + + human-signals@4.3.1: + resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} + engines: {node: '>=14.18.0'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + immediate-chunk-store@2.2.0: + resolution: {integrity: sha512-1bHBna0hCa6arRXicu91IiL9RvvkbNYLVq+mzWdaLGZC3hXvX4doh8e1dLhMKez5siu63CYgO5NrGJbRX5lbPA==} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + + ip-set@2.2.0: + resolution: {integrity: sha512-NmmY3BfY4pejh6GOqNcNWRsBNdR+I7pUVtXRgZlkZdcnLtlG4X6HNtu2FZoCGyvGRzyroP1fJ+SJZBZ65JJl/Q==} + + ip@2.0.1: + resolution: {integrity: sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + ipaddr.js@2.3.0: + resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} + engines: {node: '>= 10'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-file@1.0.0: + resolution: {integrity: sha512-ZGMuc+xA8mRnrXtmtf2l/EkIW2zaD2LSBWlaOVEF6yH4RTndHob65V4SwWWdtGKVthQfXPVKsXqw4TDUjbVxVQ==} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + join-async-iterator@1.1.1: + resolution: {integrity: sha512-ATse+nuNeKZ9K1y27LKdvPe/GCe9R/u9dw9vI248e+vILeRK3IcJP4JUPAlSmKRCDK0cKhEwfmiw4Skqx7UnGQ==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + junk@4.0.1: + resolution: {integrity: sha512-Qush0uP+G8ZScpGMZvHUiRfI0YBWuB3gVBYlI0v0vvOJt5FLicco+IkP0a50LqTTQhmts/m6tP5SWE+USyIvcQ==} + engines: {node: '>=12.20'} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + k-bucket@5.1.0: + resolution: {integrity: sha512-Fac7iINEovXIWU20GPnOMLUbjctiS+cnmyjC4zAUgvs3XPf1vo9akfCHkigftSic/jiKqKl+KA3a/vFcJbHyCg==} + + k-rpc-socket@1.11.1: + resolution: {integrity: sha512-8xtA8oqbZ6v1Niryp2/g4GxW16EQh5MvrUylQoOG+zcrDff5CKttON2XUXvMwlIHq4/2zfPVFiinAccJ+WhxoA==} + + k-rpc@5.1.0: + resolution: {integrity: sha512-FGc+n70Hcjoa/X2JTwP+jMIOpBz+pkRffHnSl9yrYiwUxg3FIgD50+u1ePfJUOnRCnx6pbjmVk5aAeB1wIijuQ==} + + last-one-wins@1.0.4: + resolution: {integrity: sha512-t+KLJFkHPQk8lfN6WBOiGkiUXoub+gnb2XTYI2P3aiISL+94xgZ1vgz1SXN/N4hthuOoLXarXfBZPUruyjQtfA==} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + limiter@1.1.5: + resolution: {integrity: sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-ip-set@3.0.1: + resolution: {integrity: sha512-ZFZt1g4Exq01SFtKjffqau+L4Qibt+51utymHHiWo8Iu/W7LYSqE7fiZ/iAZ6dIqbmeU6ICSIK02IizSScBkLQ==} + engines: {node: '>=12.20.0'} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru@3.1.0: + resolution: {integrity: sha512-5OUtoiVIGU4VXBOshidmtOsvBIvcQR6FD/RzWSvaeHyxCGB+PCUCu+52lqMfdc0h/2CLvHhZS4TwUmMQrrMbBQ==} + engines: {node: '>= 0.4.0'} + + lt_donthave@2.0.6: + resolution: {integrity: sha512-ZVcaRbZpNB6ugwa5T9gUN0Jg9XGT9cyVjZJvdbN3V27rOQ170rEs//zaQXEQkTCBhh3i/JnCRF472KWHJu74Yg==} + engines: {node: '>=12.20.0'} + + magnet-uri@6.2.0: + resolution: {integrity: sha512-O9AgdDwT771fnUj0giPYu/rACpz8173y8UXCSOdLITjOVfBenZ9H9q3FqQmveK+ORUMuD+BkKNSZP8C3+IMAKQ==} + + magnet-uri@7.0.7: + resolution: {integrity: sha512-z/+dB2NQsXaDuxVBjoPLpZT8ePaacUmoontoFheRBl++nALHYs4qV9MmhTur9e4SaMbkCR/uPX43UMzEOoeyaw==} + engines: {node: '>=12.20.0'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + memory-chunk-store@1.3.5: + resolution: {integrity: sha512-E1Xc1U4ifk/FkC2ZsWhCaW1xg9HbE/OBmQTLe2Tr9c27YPSLbW7kw1cnb3kQWD1rDtErFJHa7mB9EVrs7aTx9g==} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multer@1.4.5-lts.2: + resolution: {integrity: sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==} + engines: {node: '>= 6.0.0'} + deprecated: Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version. + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + napi-macros@2.2.2: + resolution: {integrity: sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + netmask@2.0.2: + resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} + engines: {node: '>= 0.4.0'} + + node-abi@3.85.0: + resolution: {integrity: sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==} + engines: {node: '>=10'} + + node-datachannel@0.12.0: + resolution: {integrity: sha512-pZ9FsVZpHdUKqyWynuCc9IBLkZPJMpDzpNk4YNPCizbIXHYifpYeWqSF35REHGIWi9JMCf11QzapsyQGo/Y4Ig==} + engines: {node: '>=16.0.0'} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-domexception@2.0.2: + resolution: {integrity: sha512-Qf9vHK9c5MGgUXj8SnucCIS4oEPuUstjRaMplLGeZpbWMfNV1rvEcXuwoXfN51dUfD1b4muPHPQtCx/5Dj/QAA==} + engines: {node: '>=16'} + deprecated: Use your platform's native DOMException instead + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + parse-torrent@11.0.19: + resolution: {integrity: sha512-T0lEkDdFVQsy0YxHIKjzDHSgt/yl57f3INs5jl7OZqAm77XDF0FgRgrv3LCKgSqsTOrMwYaF0t2761WKdvhgig==} + engines: {node: '>=12.20.0'} + hasBin: true + + parse-torrent@9.1.5: + resolution: {integrity: sha512-K8FXRwTOaZMI0/xuv0dpng1MVHZRtMJ0jRWBJ3qZWVNTrC1MzWUxm9QwaXDz/2qPhV2XC4UIHI92IGHwseAwaA==} + hasBin: true + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + piece-length@2.0.1: + resolution: {integrity: sha512-dBILiDmm43y0JPISWEmVGKBETQjwJe6mSU9GND+P9KW0SJGUwoU/odyH1nbalOP9i8WSYuqf1lQnaj92Bhw+Ug==} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + + pino@9.14.0: + resolution: {integrity: sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==} + hasBin: true + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + hasBin: true + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.14.1: + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} + engines: {node: '>=0.6'} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + queue-tick@1.0.1: + resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + random-access-file@4.1.2: + resolution: {integrity: sha512-GQM6R78DceZDcQod8KxlDFwXIiUvlvuy1EOzxTDsjuDjW5NlnlZi0MOk6iI4itAj/2vcvdqcEExYbVpC/dJcEw==} + + random-access-storage@3.0.2: + resolution: {integrity: sha512-Es9maUyWdJXWKckKy9s1+vT+DEgAt+PBb9lxPaake/0EDUsHehloKGv9v1zimS2V3gpFAcQXubvc1Rgci2sDPQ==} + + random-iterate@1.0.1: + resolution: {integrity: sha512-Jdsdnezu913Ot8qgKgSgs63XkAjEsnMcS1z+cC6D6TNXsUXsMxy0RpclF2pzGZTEiTXL9BiArdGTEexcv4nqcA==} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + + rc4@0.1.5: + resolution: {integrity: sha512-xdDTNV90z5x5u25Oc871Xnvu7yAr4tV7Eluh0VSvrhUkry39q1k+zkz7xroqHbRq+8PiazySHJPArqifUvz9VA==} + engines: {node: '>=0.10.0'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + 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 + + react-router-dom@6.30.2: + resolution: {integrity: sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + react-router@6.30.2: + resolution: {integrity: sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + 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 + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + record-cache@1.2.0: + resolution: {integrity: sha512-kyy3HWCez2WrotaL3O4fTn0rsIdfRKOdQQcEJ9KpvmKmbffKVvwsloX063EgRUlpJIXHiDQFhJcTbZequ2uTZw==} + + require-addon@1.2.0: + resolution: {integrity: sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==} + engines: {bare: '>=1.10.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup@4.54.0: + resolution: {integrity: sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel-limit@1.1.0: + resolution: {integrity: sha512-jJA7irRNM91jaKc3Hcl1npHsFLOXOoTkPCUL1JEa1R82O2miplXXRaGdjW/KM/98YQWDhJLiSs793CnXfblJUw==} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + run-series@1.1.9: + resolution: {integrity: sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g==} + + rusha@0.8.14: + resolution: {integrity: sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sax@1.4.3: + resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + simple-sha1@3.1.0: + resolution: {integrity: sha512-ArTptMRC1v08H8ihPD6l0wesKvMfF9e8XL5rIHPanI7kGOsSsbY514MwVu6X1PITHCTB2F08zB7cyEbfc4wQjg==} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socket.io-adapter@2.5.6: + resolution: {integrity: sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==} + + socket.io-client@4.8.3: + resolution: {integrity: sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==} + engines: {node: '>=10.0.0'} + + socket.io-parser@4.2.5: + resolution: {integrity: sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==} + engines: {node: '>=10.0.0'} + + socket.io@4.8.3: + resolution: {integrity: sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==} + engines: {node: '>=10.2.0'} + + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + speed-limiter@1.0.2: + resolution: {integrity: sha512-Ax+TbUOho84bWUc3AKqWtkIvAIVws7d6QI4oJkgH4yQ5Yil+lR3vjd/7qd51dHKGzS5bFxg0++QwyNRN7s6rZA==} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + split@1.0.1: + resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + streamx@2.22.1: + resolution: {integrity: sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==} + + string2compact@2.0.1: + resolution: {integrity: sha512-Bm/T8lHMTRXw+u83LE+OW7fXmC/wM+Mbccfdo533ajSBNxddDHlRrvxE49NdciGHgXkUQM5WYskJ7uTkbBUI0A==} + engines: {node: '>=12.20.0'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + text-decoder@1.2.3: + resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + thirty-two@1.0.2: + resolution: {integrity: sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==} + engines: {node: '>=0.2.6'} + + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + throughput@1.0.2: + resolution: {integrity: sha512-jvK1ZXuhsggjb3qYQjMiU/AVYYiTeqT5thWvYR2yuy2LGM84P5MSSyAinwHahGsdBYKR9m9HncVR/3f3nFKkxg==} + + thunky@1.1.0: + resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} + + timeout-refresh@1.0.3: + resolution: {integrity: sha512-Mz0CX4vBGM5lj8ttbIFt7o4ZMxk/9rgudJRh76EvB7xXZMur7T/cjRiH2w4Fmkq0zxf2QpM8IFvOSRn8FEu3gA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + torrent-discovery@11.0.19: + resolution: {integrity: sha512-BLhdj7o0px+u72UuhJmq6CB0LBkZOa1nwgbd5ktyTELJlvcRL8EoxSSmSpzMOIScLGgslh1uLaAy/POhLpagtg==} + engines: {node: '>=16.0.0'} + + torrent-piece@3.0.2: + resolution: {integrity: sha512-K1A5tZ3BolFrUtnBpk9iDg8av1na0OgQ7E0IlA9tj0bcsPhLhzvln+oMtMmtkqAwmUsbNCilRm2ymUdZg0rVbQ==} + engines: {node: '>=12.20.0'} + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + ts-node-dev@2.0.0: + resolution: {integrity: sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==} + engines: {node: '>=0.8.0'} + hasBin: true + peerDependencies: + node-notifier: '*' + typescript: '*' + peerDependenciesMeta: + node-notifier: + optional: true + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + tsconfig@7.0.0: + resolution: {integrity: sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uint8-util@2.2.5: + resolution: {integrity: sha512-/QxVQD7CttWpVUKVPz9znO+3Dd4BdTSnFQ7pv/4drVhC9m4BaL2LFHTkJn6EsYoxT79VDq/2Gg8L0H22PrzyMw==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + unordered-array-remove@1.0.2: + resolution: {integrity: sha512-45YsfD6svkgaCBNyvD+dFHm4qFX9g3wRSIVgWVPtm2OCnphvPxzJoe20ATsiNpNJrmzHifnxm+BN5F7gFT/4gw==} + + unordered-set@2.0.1: + resolution: {integrity: sha512-eUmNTPzdx+q/WvOHW0bgGYLWvWHNT3PTKEQLg0MAQhc0AHASHVHoP/9YytYd4RBVariqno/mEUhVZN98CmD7bg==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + 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 + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + 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 + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + ut_metadata@4.0.3: + resolution: {integrity: sha512-2tovup0VDYpT8t8+EhhhKBmbgIyiYyJQZ+Hf+/61+SvjuRS2MEeA5CiSARP4q+9/83Wu09OsGrUre/Zv6OI5NA==} + engines: {node: '>=12.20.0'} + + ut_pex@4.0.4: + resolution: {integrity: sha512-isVTbp2TKGoMOu+4Zh/i6ijpYr0VG83xjRPgCXaUjKzgXXndjCMWg32/9kZjubD+kxEXcmXMkoS8IttS9FZE8g==} + engines: {node: '>=12.20.0'} + + utf-8-validate@6.0.6: + resolution: {integrity: sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==} + engines: {node: '>=6.14.2'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + utp-native@2.5.3: + resolution: {integrity: sha512-sWTrWYXPhhWJh+cS2baPzhaZc89zwlWCfwSthUjGhLkZztyPhcQllo+XVVCbNGi7dhyRlxkWxN4NKU6FbA9Y8w==} + engines: {node: '>=8.12'} + hasBin: true + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + webrtc-polyfill@1.1.10: + resolution: {integrity: sha512-sOn0bj3/noUdzQX7rvk0jFbBurqWDGGo2ipl+WfgoOe/x3cxbGLk/ZUY+WHCISSlLaIeBumi1X3wxQZnUESExQ==} + engines: {node: '>=16.0.0'} + + webtorrent@2.8.5: + resolution: {integrity: sha512-oIjpuBrypApJ+RCZ8RRaHEncVSkt2cd25/I4Trb2sk9nlaEF92Dg1u8BCwqA4eJR7wIZQM95GyO7Wo4QTbrUUA==} + engines: {node: '>=16'} + + which-runtime@1.3.2: + resolution: {integrity: sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + 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 + + xml2js@0.6.2: + resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + + xmlhttprequest-ssl@2.1.2: + resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} + engines: {node: '>=0.4.0'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.5': {} + + '@babel/core@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.5 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + + '@babel/traverse@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@fortawesome/fontawesome-common-types@6.7.2': {} + + '@fortawesome/fontawesome-svg-core@6.7.2': + dependencies: + '@fortawesome/fontawesome-common-types': 6.7.2 + + '@fortawesome/free-solid-svg-icons@6.7.2': + dependencies: + '@fortawesome/fontawesome-common-types': 6.7.2 + + '@fortawesome/react-fontawesome@0.2.6(@fortawesome/fontawesome-svg-core@6.7.2)(react@18.3.1)': + dependencies: + '@fortawesome/fontawesome-svg-core': 6.7.2 + prop-types: 15.8.1 + react: 18.3.1 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@pinojs/redact@0.4.0': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.27)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + + '@radix-ui/react-context@1.1.2(@types/react@18.3.27)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.27)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@18.3.27)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) + + '@radix-ui/react-id@1.1.1(@types/react@18.3.27)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + + '@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) + + '@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) + + '@radix-ui/react-slot@1.2.3(@types/react@18.3.27)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.27)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.27)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.27)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.27)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.27)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.27)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.27)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.27 + + '@remix-run/router@1.23.1': {} + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.54.0': + optional: true + + '@rollup/rollup-android-arm64@4.54.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.54.0': + optional: true + + '@rollup/rollup-darwin-x64@4.54.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.54.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.54.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.54.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.54.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.54.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.54.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.54.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.54.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.54.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.54.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.54.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.54.0': + optional: true + + '@silentbot1/nat-api@0.4.9': + dependencies: + chrome-dgram: 3.0.6 + cross-fetch-ponyfill: 1.0.3 + debug: 4.4.3 + default-gateway: 7.2.2 + unordered-array-remove: 1.0.2 + xml2js: 0.6.2 + transitivePeerDependencies: + - supports-color + + '@socket.io/component-emitter@3.1.2': {} + + '@thaunknown/simple-peer@10.0.12': + dependencies: + debug: 4.4.3 + err-code: 3.0.1 + streamx: 2.22.1 + uint8-util: 2.2.5 + webrtc-polyfill: 1.1.10 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + - supports-color + + '@thaunknown/simple-websocket@9.1.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)': + dependencies: + debug: 4.4.3 + queue-microtask: 1.2.3 + streamx: 2.22.1 + uint8-util: 2.2.5 + ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - bare-abort-controller + - bufferutil + - react-native-b4a + - supports-color + - utf-8-validate + + '@thaunknown/thirty-two@1.0.5': + dependencies: + uint8-util: 2.2.5 + + '@tsconfig/node10@1.0.12': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.5 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.5 + + '@types/bcryptjs@2.4.6': {} + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.19.27 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 20.19.27 + + '@types/cookie-parser@1.4.10(@types/express@4.17.25)': + dependencies: + '@types/express': 4.17.25 + + '@types/cors@2.8.19': + dependencies: + '@types/node': 20.19.27 + + '@types/estree@1.0.8': {} + + '@types/express-serve-static-core@4.19.7': + dependencies: + '@types/node': 20.19.27 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@4.17.25': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 4.19.7 + '@types/qs': 6.14.0 + '@types/serve-static': 1.15.10 + + '@types/http-errors@2.0.5': {} + + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 20.19.27 + + '@types/mime@1.3.5': {} + + '@types/ms@2.1.0': {} + + '@types/multer@1.4.13': + dependencies: + '@types/express': 4.17.25 + + '@types/node@20.19.27': + dependencies: + undici-types: 6.21.0 + + '@types/prop-types@15.7.15': {} + + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/react-dom@18.3.7(@types/react@18.3.27)': + dependencies: + '@types/react': 18.3.27 + + '@types/react@18.3.27': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@types/send@0.17.6': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 20.19.27 + + '@types/send@1.2.1': + dependencies: + '@types/node': 20.19.27 + + '@types/serve-static@1.15.10': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 20.19.27 + '@types/send': 0.17.6 + + '@types/strip-bom@3.0.0': {} + + '@types/strip-json-comments@0.0.30': {} + + '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@20.19.27))': + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 5.4.21(@types/node@20.19.27) + transitivePeerDependencies: + - supports-color + + '@webtorrent/http-node@1.3.0': + dependencies: + freelist: 1.0.3 + http-parser-js: 0.4.13 + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + addr-to-ip-port@2.0.0: {} + + agent-base@7.1.4: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + append-field@1.0.0: {} + + arg@4.1.3: {} + + arg@5.0.2: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + array-flatten@1.1.1: {} + + asynckit@0.4.0: {} + + atomic-sleep@1.0.0: {} + + autoprefixer@10.4.23(postcss@8.5.6): + dependencies: + browserslist: 4.28.1 + caniuse-lite: 1.0.30001762 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + axios-cookiejar-support@5.0.5(axios@1.13.2)(tough-cookie@4.1.4): + dependencies: + axios: 1.13.2 + http-cookie-agent: 6.0.8(tough-cookie@4.1.4) + tough-cookie: 4.1.4 + transitivePeerDependencies: + - undici + + axios@1.13.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + b4a@1.7.3: {} + + balanced-match@1.0.2: {} + + bare-addon-resolve@1.9.6(bare-url@2.3.2): + dependencies: + bare-module-resolve: 1.12.0(bare-url@2.3.2) + bare-semver: 1.0.2 + optionalDependencies: + bare-url: 2.3.2 + optional: true + + bare-events@2.8.2: {} + + bare-fs@4.5.2: + dependencies: + bare-events: 2.8.2 + bare-path: 3.0.0 + bare-stream: 2.7.0(bare-events@2.8.2) + bare-url: 2.3.2 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + bare-module-resolve@1.12.0(bare-url@2.3.2): + dependencies: + bare-semver: 1.0.2 + optionalDependencies: + bare-url: 2.3.2 + optional: true + + bare-os@3.6.2: {} + + bare-path@3.0.0: + dependencies: + bare-os: 3.6.2 + + bare-semver@1.0.2: + optional: true + + bare-stream@2.7.0(bare-events@2.8.2): + dependencies: + streamx: 2.22.1 + optionalDependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + bare-url@2.3.2: + dependencies: + bare-path: 3.0.0 + + base64-arraybuffer@1.0.2: {} + + base64-js@1.5.1: {} + + base64id@2.0.0: {} + + baseline-browser-mapping@2.9.11: {} + + bcryptjs@2.4.3: {} + + bencode@2.0.3: {} + + bencode@4.0.0: + dependencies: + uint8-util: 2.2.5 + + bep53-range@1.1.1: {} + + bep53-range@2.0.0: {} + + binary-extensions@2.3.0: {} + + bitfield@4.2.0: {} + + bittorrent-dht@11.0.11: + 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 + transitivePeerDependencies: + - react-native-b4a + - supports-color + + bittorrent-lsd@2.0.0: + dependencies: + chrome-dgram: 3.0.6 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + bittorrent-peerid@1.3.6: {} + + bittorrent-protocol@4.1.21: + 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 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + - supports-color + + bittorrent-tracker@11.2.2: + dependencies: + '@thaunknown/simple-peer': 10.0.12 + '@thaunknown/simple-websocket': 9.1.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) + 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.4.3 + 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.7 + string2compact: 2.0.1 + uint8-util: 2.2.5 + unordered-array-remove: 1.0.2 + ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) + optionalDependencies: + bufferutil: 4.1.0 + utf-8-validate: 6.0.6 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + - supports-color + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + blob-to-buffer@1.2.9: {} + + block-iterator@1.1.1: {} + + body-parser@1.20.4: + 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.1 + raw-body: 2.5.3 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.11 + caniuse-lite: 1.0.30001762 + electron-to-chromium: 1.5.267 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + buffer-equal-constant-time@1.0.1: {} + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bufferutil@4.1.0: + dependencies: + node-gyp-build: 4.8.4 + optional: true + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + bytes@3.1.2: {} + + cache-chunk-store@3.2.2: + dependencies: + lru: 3.1.0 + queue-microtask: 1.2.3 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001762: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chownr@1.1.4: {} + + chrome-dgram@3.0.6: + dependencies: + inherits: 2.0.4 + run-series: 1.1.9 + + chrome-dns@1.0.1: + dependencies: + chrome-net: 3.3.4 + + chrome-net@3.3.4: + dependencies: + inherits: 2.0.4 + + chunk-store-iterator@1.0.4: + dependencies: + block-iterator: 1.1.1 + + clsx@2.1.1: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@4.1.1: {} + + compact2string@1.4.1: + dependencies: + ipaddr.js: 2.3.0 + + concat-map@0.0.1: {} + + concat-stream@1.6.2: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 2.3.8 + typedarray: 0.0.6 + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + convert-source-map@2.0.0: {} + + cookie-parser@1.4.7: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.6 + + cookie-signature@1.0.6: {} + + cookie-signature@1.0.7: {} + + cookie@0.7.2: {} + + core-util-is@1.0.3: {} + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cpus@1.0.3: {} + + create-require@1.1.1: {} + + create-torrent@6.1.0: + 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 + + cross-fetch-ponyfill@1.0.3: + dependencies: + abort-controller: 3.0.0 + node-fetch: 3.3.2 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + data-uri-to-buffer@4.0.1: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + + default-gateway@7.2.2: + dependencies: + execa: 7.2.0 + + delayed-stream@1.0.0: {} + + depd@2.0.0: {} + + destroy@1.2.0: {} + + detect-libc@2.1.2: {} + + detect-node-es@1.1.0: {} + + didyoumean@1.2.2: {} + + diff@4.0.2: {} + + dlv@1.1.3: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + dynamic-dedupe@0.3.0: + dependencies: + xtend: 4.0.2 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.267: {} + + encodeurl@2.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + engine.io-client@6.6.4(bufferutil@4.1.0)(utf-8-validate@6.0.6): + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3 + engine.io-parser: 5.2.3 + ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) + xmlhttprequest-ssl: 2.1.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + engine.io-parser@5.2.3: {} + + engine.io@6.6.5(bufferutil@4.1.0)(utf-8-validate@6.0.6): + dependencies: + '@types/cors': 2.8.19 + '@types/node': 20.19.27 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.7.2 + cors: 2.8.5 + debug: 4.4.3 + engine.io-parser: 5.2.3 + ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + err-code@3.0.1: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + event-target-shim@5.0.1: {} + + execa@7.2.0: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 4.3.1 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 3.0.7 + strip-final-newline: 3.0.0 + + expand-template@2.0.3: {} + + express-rate-limit@7.5.1(express@4.22.1): + dependencies: + express: 4.22.1 + + express@4.22.1: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.4 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.2 + fresh: 0.5.2 + http-errors: 2.0.1 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.14.1 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.2 + serve-static: 1.16.3 + setprototypeof: 1.2.0 + statuses: 2.0.2 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-fifo@1.3.2: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-readable-async-iterator@2.0.0: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + filename-reserved-regex@3.0.0: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@1.3.2: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + follow-redirects@1.15.11: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + forwarded@0.2.0: {} + + fraction.js@5.3.4: {} + + freelist@1.0.3: {} + + fresh@0.5.2: {} + + fs-chunk-store@5.0.0(bare-url@2.3.2): + dependencies: + filename-reserved-regex: 3.0.0 + queue-microtask: 1.2.3 + random-access-file: 4.1.2(bare-url@2.3.2) + run-parallel: 1.2.0 + thunky: 1.1.0 + uint8-util: 2.2.5 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - bare-url + - react-native-b4a + + fs-constants@1.0.0: {} + + fs-native-extensions@1.4.5(bare-url@2.3.2): + dependencies: + require-addon: 1.2.0(bare-url@2.3.2) + which-runtime: 1.3.2 + transitivePeerDependencies: + - bare-url + optional: true + + fs.realpath@1.0.0: {} + + fsa-chunk-store@1.3.0: + dependencies: + filename-reserved-regex: 3.0.0 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-nonce@1.0.1: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stdin@8.0.0: {} + + get-stdin@9.0.0: {} + + get-stream@6.0.1: {} + + github-from-package@0.0.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + http-cookie-agent@6.0.8(tough-cookie@4.1.4): + dependencies: + agent-base: 7.1.4 + tough-cookie: 4.1.4 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + http-parser-js@0.4.13: {} + + human-signals@4.3.1: {} + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + immediate-chunk-store@2.2.0: + dependencies: + queue-microtask: 1.2.3 + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ip-address@10.1.0: {} + + ip-set@2.2.0: + dependencies: + ip: 2.0.1 + + ip@2.0.1: {} + + ipaddr.js@1.9.1: {} + + ipaddr.js@2.3.0: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-file@1.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-stream@3.0.0: {} + + isarray@1.0.0: {} + + isexe@2.0.0: {} + + jiti@1.21.7: {} + + join-async-iterator@1.1.1: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.3 + + junk@4.0.1: {} + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + k-bucket@5.1.0: + dependencies: + randombytes: 2.1.0 + + k-rpc-socket@1.11.1: + dependencies: + bencode: 2.0.3 + chrome-dgram: 3.0.6 + chrome-dns: 1.0.1 + chrome-net: 3.3.4 + + k-rpc@5.1.0: + dependencies: + k-bucket: 5.1.0 + k-rpc-socket: 1.11.1 + randombytes: 2.1.0 + + last-one-wins@1.0.4: {} + + lilconfig@3.1.3: {} + + limiter@1.1.5: {} + + lines-and-columns@1.2.4: {} + + load-ip-set@3.0.1: + dependencies: + cross-fetch-ponyfill: 1.0.3 + ip-set: 2.2.0 + netmask: 2.0.2 + once: 1.4.0 + queue-microtask: 1.2.3 + split: 1.0.1 + + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.once@4.1.1: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lru@3.1.0: + dependencies: + inherits: 2.0.4 + + lt_donthave@2.0.6: + dependencies: + debug: 4.4.3 + unordered-array-remove: 1.0.2 + transitivePeerDependencies: + - supports-color + + magnet-uri@6.2.0: + dependencies: + bep53-range: 1.1.1 + thirty-two: 1.0.2 + + magnet-uri@7.0.7: + dependencies: + '@thaunknown/thirty-two': 1.0.5 + bep53-range: 2.0.0 + uint8-util: 2.2.5 + + make-error@1.3.6: {} + + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + memory-chunk-store@1.3.5: + dependencies: + queue-microtask: 1.2.3 + + merge-descriptors@1.0.3: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + methods@1.1.2: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + mime@3.0.0: {} + + mimic-fn@4.0.0: {} + + mimic-response@3.1.0: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimist@1.2.8: {} + + mkdirp-classic@0.5.3: {} + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + + mkdirp@1.0.4: {} + + ms@2.0.0: {} + + ms@2.1.3: {} + + multer@1.4.5-lts.2: + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 1.6.2 + mkdirp: 0.5.6 + object-assign: 4.1.1 + type-is: 1.6.18 + xtend: 4.0.2 + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + napi-build-utils@2.0.0: {} + + napi-macros@2.2.2: + optional: true + + negotiator@0.6.3: {} + + netmask@2.0.2: {} + + node-abi@3.85.0: + dependencies: + semver: 7.7.3 + + node-datachannel@0.12.0: + dependencies: + node-domexception: 2.0.2 + prebuild-install: 7.1.3 + + node-domexception@1.0.0: {} + + node-domexception@2.0.2: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + node-gyp-build@4.8.4: + optional: true + + node-releases@2.0.27: {} + + normalize-path@3.0.0: {} + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + object-inspect@1.13.4: {} + + on-exit-leak-free@2.1.2: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + parse-torrent@11.0.19: + 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 + + parse-torrent@9.1.5: + dependencies: + bencode: 2.0.3 + blob-to-buffer: 1.2.9 + get-stdin: 8.0.0 + magnet-uri: 6.2.0 + queue-microtask: 1.2.3 + simple-get: 4.0.1 + simple-sha1: 3.1.0 + + parseurl@1.3.3: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-parse@1.0.7: {} + + path-to-regexp@0.1.12: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + piece-length@2.0.1: {} + + pify@2.3.0: {} + + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.0.0: {} + + pino@9.14.0: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.0.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 3.1.0 + + pirates@4.0.7: {} + + postcss-import@15.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.11 + + postcss-js@4.1.0(postcss@8.5.6): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.6 + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.6 + + postcss-nested@6.2.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.85.0 + pump: 3.0.3 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + + process-nextick-args@2.0.1: {} + + process-warning@5.0.0: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-from-env@1.1.0: {} + + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + punycode@2.3.1: {} + + qs@6.14.1: + dependencies: + side-channel: 1.1.0 + + querystringify@2.2.0: {} + + queue-microtask@1.2.3: {} + + queue-tick@1.0.1: {} + + quick-format-unescaped@4.0.4: {} + + random-access-file@4.1.2(bare-url@2.3.2): + dependencies: + bare-fs: 4.5.2 + bare-path: 3.0.0 + random-access-storage: 3.0.2 + optionalDependencies: + fs-native-extensions: 1.4.5(bare-url@2.3.2) + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - bare-url + - react-native-b4a + + random-access-storage@3.0.2: + dependencies: + bare-events: 2.8.2 + queue-tick: 1.0.1 + transitivePeerDependencies: + - bare-abort-controller + + random-iterate@1.0.1: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + range-parser@1.2.1: {} + + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + rc4@0.1.5: {} + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-is@16.13.1: {} + + react-refresh@0.17.0: {} + + react-remove-scroll-bar@2.3.8(@types/react@18.3.27)(react@18.3.1): + dependencies: + react: 18.3.1 + react-style-singleton: 2.2.3(@types/react@18.3.27)(react@18.3.1) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.27 + + react-remove-scroll@2.7.2(@types/react@18.3.27)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.8(@types/react@18.3.27)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.27)(react@18.3.1) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@18.3.27)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.3.27)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + + react-router-dom@6.30.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@remix-run/router': 1.23.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 6.30.2(react@18.3.1) + + react-router@6.30.2(react@18.3.1): + dependencies: + '@remix-run/router': 1.23.1 + react: 18.3.1 + + react-style-singleton@2.2.3(@types/react@18.3.27)(react@18.3.1): + dependencies: + get-nonce: 1.0.1 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.27 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + real-require@0.2.0: {} + + record-cache@1.2.0: + dependencies: + b4a: 1.7.3 + transitivePeerDependencies: + - react-native-b4a + + require-addon@1.2.0(bare-url@2.3.2): + dependencies: + bare-addon-resolve: 1.9.6(bare-url@2.3.2) + transitivePeerDependencies: + - bare-url + optional: true + + requires-port@1.0.0: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + + rollup@4.54.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.54.0 + '@rollup/rollup-android-arm64': 4.54.0 + '@rollup/rollup-darwin-arm64': 4.54.0 + '@rollup/rollup-darwin-x64': 4.54.0 + '@rollup/rollup-freebsd-arm64': 4.54.0 + '@rollup/rollup-freebsd-x64': 4.54.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.54.0 + '@rollup/rollup-linux-arm-musleabihf': 4.54.0 + '@rollup/rollup-linux-arm64-gnu': 4.54.0 + '@rollup/rollup-linux-arm64-musl': 4.54.0 + '@rollup/rollup-linux-loong64-gnu': 4.54.0 + '@rollup/rollup-linux-ppc64-gnu': 4.54.0 + '@rollup/rollup-linux-riscv64-gnu': 4.54.0 + '@rollup/rollup-linux-riscv64-musl': 4.54.0 + '@rollup/rollup-linux-s390x-gnu': 4.54.0 + '@rollup/rollup-linux-x64-gnu': 4.54.0 + '@rollup/rollup-linux-x64-musl': 4.54.0 + '@rollup/rollup-openharmony-arm64': 4.54.0 + '@rollup/rollup-win32-arm64-msvc': 4.54.0 + '@rollup/rollup-win32-ia32-msvc': 4.54.0 + '@rollup/rollup-win32-x64-gnu': 4.54.0 + '@rollup/rollup-win32-x64-msvc': 4.54.0 + fsevents: 2.3.3 + + run-parallel-limit@1.1.0: + dependencies: + queue-microtask: 1.2.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + run-series@1.1.9: {} + + rusha@0.8.14: {} + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + sax@1.4.3: {} + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + semver@6.3.1: {} + + semver@7.7.3: {} + + send@0.19.2: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.3: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.2 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + simple-sha1@3.1.0: + dependencies: + queue-microtask: 1.2.3 + rusha: 0.8.14 + + smart-buffer@4.2.0: {} + + socket.io-adapter@2.5.6(bufferutil@4.1.0)(utf-8-validate@6.0.6): + dependencies: + debug: 4.4.3 + ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-client@4.8.3(bufferutil@4.1.0)(utf-8-validate@6.0.6): + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3 + engine.io-client: 6.6.4(bufferutil@4.1.0)(utf-8-validate@6.0.6) + socket.io-parser: 4.2.5 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.5: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + socket.io@4.8.3(bufferutil@4.1.0)(utf-8-validate@6.0.6): + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.4.3 + engine.io: 6.6.5(bufferutil@4.1.0)(utf-8-validate@6.0.6) + socket.io-adapter: 2.5.6(bufferutil@4.1.0)(utf-8-validate@6.0.6) + socket.io-parser: 4.2.5 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socks@2.8.7: + dependencies: + ip-address: 10.1.0 + smart-buffer: 4.2.0 + + sonic-boom@4.2.0: + dependencies: + atomic-sleep: 1.0.0 + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + speed-limiter@1.0.2: + dependencies: + limiter: 1.1.5 + streamx: 2.22.1 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + split2@4.2.0: {} + + split@1.0.1: + dependencies: + through: 2.3.8 + + statuses@2.0.2: {} + + streamsearch@1.1.0: {} + + streamx@2.22.1: + dependencies: + fast-fifo: 1.3.2 + text-decoder: 1.2.3 + optionalDependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + string2compact@2.0.1: + dependencies: + addr-to-ip-port: 2.0.0 + ipaddr.js: 2.3.0 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-bom@3.0.0: {} + + strip-final-newline@3.0.0: {} + + strip-json-comments@2.0.1: {} + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwindcss@3.4.19: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-import: 15.1.0(postcss@8.5.6) + postcss-js: 4.1.0(postcss@8.5.6) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6) + postcss-nested: 6.2.0(postcss@8.5.6) + postcss-selector-parser: 6.1.2 + resolve: 1.22.11 + sucrase: 3.35.1 + transitivePeerDependencies: + - tsx + - yaml + + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.3 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + text-decoder@1.2.3: + dependencies: + b4a: 1.7.3 + transitivePeerDependencies: + - react-native-b4a + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + thirty-two@1.0.2: {} + + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + + through@2.3.8: {} + + throughput@1.0.2: {} + + thunky@1.1.0: {} + + timeout-refresh@1.0.3: + optional: true + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + torrent-discovery@11.0.19: + 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 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + - supports-color + + torrent-piece@3.0.2: + dependencies: + uint8-util: 2.2.5 + + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tree-kill@1.2.2: {} + + ts-interface-checker@0.1.13: {} + + ts-node-dev@2.0.0(@types/node@20.19.27)(typescript@5.9.3): + dependencies: + chokidar: 3.6.0 + dynamic-dedupe: 0.3.0 + minimist: 1.2.8 + mkdirp: 1.0.4 + resolve: 1.22.11 + rimraf: 2.7.1 + source-map-support: 0.5.21 + tree-kill: 1.2.2 + ts-node: 10.9.2(@types/node@20.19.27)(typescript@5.9.3) + tsconfig: 7.0.0 + typescript: 5.9.3 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + - '@types/node' + + ts-node@10.9.2(@types/node@20.19.27)(typescript@5.9.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.19.27 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + tsconfig@7.0.0: + dependencies: + '@types/strip-bom': 3.0.0 + '@types/strip-json-comments': 0.0.30 + strip-bom: 3.0.0 + strip-json-comments: 2.0.1 + + tslib@2.8.1: {} + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + typedarray@0.0.6: {} + + typescript@5.9.3: {} + + uint8-util@2.2.5: + dependencies: + base64-arraybuffer: 1.0.2 + + undici-types@6.21.0: {} + + universalify@0.2.0: {} + + unordered-array-remove@1.0.2: {} + + unordered-set@2.0.1: + optional: true + + unpipe@1.0.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + use-callback-ref@1.3.3(@types/react@18.3.27)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.27 + + use-sidecar@1.1.3(@types/react@18.3.27)(react@18.3.1): + dependencies: + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.27 + + use-sync-external-store@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + + ut_metadata@4.0.3: + dependencies: + bencode: 4.0.0 + bitfield: 4.2.0 + debug: 4.4.3 + uint8-util: 2.2.5 + transitivePeerDependencies: + - supports-color + + ut_pex@4.0.4: + dependencies: + bencode: 4.0.0 + compact2string: 1.4.1 + string2compact: 2.0.1 + + utf-8-validate@6.0.6: + dependencies: + node-gyp-build: 4.8.4 + optional: true + + util-deprecate@1.0.2: {} + + utils-merge@1.0.1: {} + + utp-native@2.5.3: + dependencies: + napi-macros: 2.2.2 + node-gyp-build: 4.8.4 + readable-stream: 3.6.2 + timeout-refresh: 1.0.3 + unordered-set: 2.0.1 + optional: true + + v8-compile-cache-lib@3.0.1: {} + + vary@1.1.2: {} + + vite@5.4.21(@types/node@20.19.27): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.54.0 + optionalDependencies: + '@types/node': 20.19.27 + fsevents: 2.3.3 + + web-streams-polyfill@3.3.3: {} + + webrtc-polyfill@1.1.10: + dependencies: + node-datachannel: 0.12.0 + node-domexception: 1.0.0 + + webtorrent@2.8.5(bare-url@2.3.2): + dependencies: + '@silentbot1/nat-api': 0.4.9 + '@thaunknown/simple-peer': 10.0.12 + '@webtorrent/http-node': 1.3.0 + addr-to-ip-port: 2.0.0 + bitfield: 4.2.0 + bittorrent-dht: 11.0.11 + bittorrent-protocol: 4.1.21 + 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.3 + escape-html: 1.0.3 + fs-chunk-store: 5.0.0(bare-url@2.3.2) + 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.6 + memory-chunk-store: 1.3.5 + mime: 3.0.0 + once: 1.4.0 + parse-torrent: 11.0.19 + pump: 3.0.3 + 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.19 + 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 + optionalDependencies: + utp-native: 2.5.3 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - bare-url + - react-native-b4a + - supports-color + + which-runtime@1.3.2: + optional: true + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrappy@1.0.2: {} + + ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@6.0.6): + optionalDependencies: + bufferutil: 4.1.0 + utf-8-validate: 6.0.6 + + xml2js@0.6.2: + dependencies: + sax: 1.4.3 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + + xmlhttprequest-ssl@2.1.2: {} + + xtend@4.0.2: {} + + yallist@3.1.1: {} + + yn@3.1.1: {} + + zod@3.25.76: {} + + zustand@4.5.7(@types/react@18.3.27)(react@18.3.1): + dependencies: + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.27 + react: 18.3.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..06b6051 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - "apps/*"