From a518a7d40841882d584320ea9477e9fbcf30d0d3 Mon Sep 17 00:00:00 2001 From: Folist Date: Sat, 22 Aug 2026 15:32:57 +0300 Subject: [PATCH] =?UTF-8?q?Headless-=D0=BC=D0=BE=D1=81=D1=82:=20=D0=B2?= =?UTF-8?q?=D0=B5=D0=B1-=D0=BF=D0=B0=D0=BD=D0=B5=D0=BB=D1=8C=20=D0=B2?= =?UTF-8?q?=D1=8B=D1=80=D0=B5=D0=B7=D0=B0=D0=BD=D0=B0,=20=D0=BE=D0=BF?= =?UTF-8?q?=D1=86=D0=B8=D0=BE=D0=BD=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D0=B5=20?= =?UTF-8?q?=D1=81=D1=82=D0=B8=D0=BA=D0=B5=D1=80=D1=8B=20(slim/full),=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B4=D0=BF=D0=B8=D1=81=D1=8C=20=D0=BE=D0=B1=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Вырезана веб-панель целиком (express/ws/react/vite/tailwind, TLS-серт, /api/*, API_KEY, порт 3000) — мост теперь headless: MAX по исходящему сокету, Telegram long-polling, управление и авторизация только в боте (/login, /panel). Убрана поверхность атаки, bundle меньше. Анимированные стикеры (.tgs) стали опциональными: build-arg STICKERS=full|slim. full (деф.) ставит Chromium+ffmpeg (образ ~1.7 ГБ); slim пропускает их (~0.3 ГБ, быстрая сборка), анимированные уходят статической картинкой (thumbnail). setup.sh спрашивает при установке (30с таймаут, не тормозит). Подпись обновлений (Phase C): update.sh/release.sh проверяют GPG-подпись релизного тега при наличии pinned-ключа release-signing-key.asc (транспорт GitHub/зеркало недоверенный, важна только подпись мейнтейнера). Активируется после добавления публичного ключа. --- .env.example | 20 +- Dockerfile | 20 +- docker-compose.yml | 5 +- index.html | 12 - install.sh | 9 +- package-lock.json | 2047 +--------------------------------- package.json | 21 +- scripts/release.sh | 18 +- setup.sh | 245 +--- src/App.tsx | 674 ----------- src/api.ts | 28 - src/bridge/lottie.ts | 11 +- src/bridge/panel.ts | 21 +- src/bridge/sync.ts | 89 +- src/index.css | 1 - src/main.tsx | 10 - src/server/app.ts | 421 +------ src/server/authMiddleware.ts | 20 - src/server/config.ts | 11 - tsconfig.json | 3 +- tsconfig.web.json | 16 - update.sh | 30 + vite.config.ts | 7 - 23 files changed, 188 insertions(+), 3551 deletions(-) delete mode 100644 index.html delete mode 100644 src/App.tsx delete mode 100644 src/api.ts delete mode 100644 src/index.css delete mode 100644 src/main.tsx delete mode 100644 src/server/authMiddleware.ts delete mode 100644 tsconfig.web.json delete mode 100644 vite.config.ts diff --git a/.env.example b/.env.example index 1c4a75b..db539de 100644 --- a/.env.example +++ b/.env.example @@ -2,18 +2,6 @@ # Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" MAX_SESSION_KEY= -# Shared secret required on every /api/* route (and the /ws upgrade) except /api/health. -# Generate with: node -e "console.log(require('crypto').randomBytes(24).toString('hex'))" -API_KEY= - -PORT=3000 - -# Веб-панель по умолчанию работает по HTTPS с самоподписанным сертификатом -# (генерируется при первом старте в .data/tls; браузер один раз предупредит о -# неизвестном издателе — это ожидаемо). Раскомментируйте off, только если панель -# стоит за вашим собственным reverse proxy с настоящим TLS. -# PANEL_TLS=off - # Опционально: адрес сервера MAX. По умолчанию (пусто) подключаемся по имени # api2.oneme.ru — это единственный рабочий путь для IPv6-only хостов (резолв имени # даёт DNS64/NAT64; захардкоженный IPv4-литерал этот путь обходит и рвётся раз в ~несколько @@ -29,7 +17,9 @@ TARGET_TELEGRAM_GROUP= # Опционально: прокси для Telegram, если сервер выходит в Telegram только через него # (напр. домашний сервер, где прямой доступ к api.telegram.org закрыт). MAX всегда # идёт напрямую, минуя прокси. Форматы: socks5://[логин:пароль@]хост:порт или -# http://[логин:пароль@]хост:порт. Пусто — без прокси. То же можно задать в веб-панели -# (вкладка Configuration → Telegram-прокси), там значение сохраняется в ./data и -# перекрывает эту переменную. +# http://[логин:пароль@]хост:порт. Пусто — без прокси. TELEGRAM_PROXY= + +# Анимированные стикеры (.tgs) — это НЕ переменная .env, а build-arg образа: setup.sh спросит +# при установке (full = проигрываются видео, +~1.4 ГБ; slim = уходят картинкой). Пересобрать +# в нужном режиме вручную: STICKERS=slim docker compose build && docker compose up -d diff --git a/Dockerfile b/Dockerfile index 751e29b..7bb976e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,15 +14,15 @@ ENV NODE_ENV=production # commit is actually running vs what's latest on GitHub. ARG GIT_COMMIT=unknown ENV GIT_COMMIT=$GIT_COMMIT -# chromium + ffmpeg: rendering Telegram's animated (.tgs/Lottie) stickers to a -# short WebM so they can go through the ordinary VIDEO_UPLOAD pipeline — MAX has -# no confirmed native sticker-upload opcode, but its own animated stickers arrive -# as autoplaying VIDEO attaches, so this gets the same "plays in the feed" result. -# Uses puppeteer-core (no bundled Chromium download) against this system package -# instead, keeping the image smaller. -# openssl: generates the web panel's self-signed TLS certificate on first start -# (see loadOrCreatePanelCert in src/server/app.ts). -RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates chromium ffmpeg openssl && rm -rf /var/lib/apt/lists/* +# Animated-sticker rendering (.tgs/Lottie → short WebM via headless Chromium + ffmpeg, so it can go +# through the ordinary VIDEO_UPLOAD pipeline and autoplay in the feed like MAX's own animated stickers) +# is OPTIONAL — chromium + ffmpeg together weigh ~1.4 GB. Build with --build-arg STICKERS=slim to skip +# them (image ~0.3 GB); animated stickers then relay as their static thumbnail instead of a playable +# video. STICKERS=full (default) installs them. ca-certificates is always needed for TLS. +ARG STICKERS=full +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \ + && if [ "$STICKERS" = "full" ]; then apt-get install -y --no-install-recommends chromium ffmpeg; fi \ + && rm -rf /var/lib/apt/lists/* ENV CHROMIUM_PATH=/usr/bin/chromium # MAX's TLS chain goes through Russia's state CA (Минцифры "Russian Trusted # Root/Sub CA"), which no standard trust store includes. Deliberately NOT @@ -36,5 +36,5 @@ COPY certs ./certs COPY package.json package-lock.json ./ RUN npm ci --omit=dev COPY --from=build /app/dist ./dist -EXPOSE 3000 +# Headless bridge (v0.4): no inbound web server — MAX is an outbound socket, Telegram is long-polled. CMD ["node", "dist/server.mjs"] diff --git a/docker-compose.yml b/docker-compose.yml index 45da022..5a09318 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,9 @@ services: context: . args: GIT_COMMIT: ${GIT_COMMIT:-unknown} + # full (default) = animated stickers play as video (needs Chromium+ffmpeg, ~1.4 GB extra). + # slim = animated stickers relay as a static image; much smaller/faster image. Set by setup.sh. + STICKERS: ${STICKERS:-full} restart: unless-stopped # Hard cap on container stdout: the default json-file driver grows without # limit between container recreations. 10m x 3 bounds it at ~30 MB total; @@ -14,8 +17,6 @@ services: options: max-size: "10m" max-file: "3" - ports: - - "${PORT:-3000}:${PORT:-3000}" env_file: - .env volumes: diff --git a/index.html b/index.html deleted file mode 100644 index bcc9ca7..0000000 --- a/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Telemax Bridge - - -
- - - diff --git a/install.sh b/install.sh index df43802..2bdd10a 100755 --- a/install.sh +++ b/install.sh @@ -54,11 +54,12 @@ apt-get update -y /dev/null 2>&1; then +# for it manually — see setup.sh). gnupg: used by update.sh to verify the signed +# release tag before building (v0.4 signed-update trust — see update.sh). +for pkg in git jq gnupg; do + if ! command -v "${pkg/gnupg/gpg}" >/dev/null 2>&1; then apt_get install -y "$pkg" fi done diff --git a/package-lock.json b/package-lock.json index 1722cfb..a146dff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,14 @@ { "name": "telemax", - "version": "0.1.0", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "telemax", - "version": "0.1.0", + "version": "0.4.0", "dependencies": { "dotenv": "^17.2.3", - "express": "^4.21.2", "fzstd": "^0.1.1", "https-proxy-agent": "^9.1.0", "lottie-web": "^5.13.0", @@ -18,310 +17,16 @@ "puppeteer-core": "^25.6.0", "socks-proxy-agent": "^10.1.0", "telegraf": "^4.16.3", - "undici": "^8.10.0", - "ws": "^8.21.1" + "undici": "^8.10.0" }, "devDependencies": { - "@tailwindcss/vite": "^4.1.14", - "@types/express": "^4.17.21", "@types/node": "^22.20.1", - "@types/react": "^19.2.18", - "@types/react-dom": "^19.2.4", - "@types/ws": "^8.18.1", - "@vitejs/plugin-react": "^5.0.4", "esbuild": "^0.25.0", - "lucide-react": "^0.546.0", - "react": "^19.0.1", - "react-dom": "^19.0.1", - "tailwindcss": "^4.1.14", "tsx": "^4.21.0", "typescript": "~5.8.2", - "vite": "^6.2.3", "vitest": "^4.1.10" } }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", - "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.8", - "@babel/types": "^7.29.8", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", - "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", - "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.8" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", - "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", - "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", - "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.8", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.8", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.8", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", - "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -764,38 +469,6 @@ "node": ">=18" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -803,17 +476,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", @@ -937,13 +599,6 @@ } } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", - "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", - "dev": true, - "license": "MIT" - }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.62.4", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", @@ -1301,406 +956,12 @@ "dev": true, "license": "MIT" }, - "node_modules/@tailwindcss/node": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", - "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.24.1", - "jiti": "^2.7.0", - "lightningcss": "1.32.0", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.3.3" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", - "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.3.3", - "@tailwindcss/oxide-darwin-arm64": "4.3.3", - "@tailwindcss/oxide-darwin-x64": "4.3.3", - "@tailwindcss/oxide-freebsd-x64": "4.3.3", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", - "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", - "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", - "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", - "@tailwindcss/oxide-linux-x64-musl": "4.3.3", - "@tailwindcss/oxide-wasm32-wasi": "4.3.3", - "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", - "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", - "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", - "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", - "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", - "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", - "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", - "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", - "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", - "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", - "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", - "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.11.1", - "@emnapi/runtime": "^1.11.1", - "@emnapi/wasi-threads": "^1.2.2", - "@napi-rs/wasm-runtime": "^1.1.4", - "@tybys/wasm-util": "^0.10.2", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.11.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.11.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.1", - "dev": true, - "inBundle": true, - "license": "0BSD", - "optional": true - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", - "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", - "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/vite": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", - "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tailwindcss/node": "4.3.3", - "@tailwindcss/oxide": "4.3.3", - "tailwindcss": "4.3.3" - }, - "peerDependencies": { - "vite": "^5.2.0 || ^6 || ^7 || ^8" - } - }, "node_modules/@telegraf/types": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@telegraf/types/-/types-7.1.0.tgz", "integrity": "sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw==", "license": "MIT" }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -1712,16 +973,6 @@ "assertion-error": "^2.0.1" } }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -1736,46 +987,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/express": { - "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", - "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "^1" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.9", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz", - "integrity": "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/node": { "version": "22.20.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", @@ -1786,104 +997,6 @@ "undici-types": "~6.21.0" } }, - "node_modules/@types/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "19.2.18", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", - "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" - } - }, - "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", - "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.29.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-rc.3", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.18.0" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, "node_modules/@vitest/expect": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", @@ -2009,19 +1122,6 @@ "node": ">=6.5" } }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/agent-base": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", @@ -2055,12 +1155,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -2071,92 +1165,6 @@ "node": ">=12" } }, - "node_modules/baseline-browser-mapping": { - "version": "2.11.12", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz", - "integrity": "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/body-parser": { - "version": "1.20.6", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", - "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.15.1", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/browserslist": { - "version": "4.28.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", - "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.44", - "caniuse-lite": "^1.0.30001806", - "electron-to-chromium": "^1.5.393", - "node-releases": "^2.0.51", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, "node_modules/buffer-alloc": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", @@ -2179,65 +1187,6 @@ "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", "license": "MIT" }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001806", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", - "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -2295,27 +1244,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2323,28 +1251,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "license": "MIT" - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2362,31 +1268,12 @@ } } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "devOptional": true, "license": "Apache-2.0", + "optional": true, "engines": { "node": ">=8" } @@ -2409,80 +1296,12 @@ "url": "https://dotenvx.com" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.401", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.401.tgz", - "integrity": "sha512-H6ViHN68nGYlChEvlIU67fn8O2/tpbWQPwck98yaJmh+08LSvHiydzDQ6oXNccLU3kNRVIRS9A4mA7CG+i6fLQ==", - "dev": true, - "license": "ISC" - }, "node_modules/emoji-regex": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "license": "MIT" }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.24.5", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", - "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/es-module-lexer": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", @@ -2490,18 +1309,6 @@ "dev": true, "license": "MIT" }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -2553,12 +1360,6 @@ "node": ">=6" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -2569,15 +1370,6 @@ "@types/estree": "^1.0.0" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -2597,67 +1389,6 @@ "node": ">=12.0.0" } }, - "node_modules/express": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", - "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.5", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.15.1", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2676,57 +1407,6 @@ } } }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2742,31 +1422,12 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/fzstd": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/fzstd/-/fzstd-0.1.1.tgz", "integrity": "sha512-dkuVSOKKwh3eas5VkJy1AW1vFpet8TA/fGmVA5krThl8YcOVE/8ZIoEA1+U1vEn5ckxxhLirSdY837azmbaNHA==", "license": "MIT" }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -2788,106 +1449,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/https-proxy-agent": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.1.0.tgz", @@ -2902,24 +1463,6 @@ "node": ">= 20" } }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, "node_modules/ip-address": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", @@ -2929,64 +1472,26 @@ "node": ">= 12" } }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, "license": "MPL-2.0", + "optional": true, + "peer": true, "dependencies": { "detect-libc": "^2.0.3" }, @@ -3024,6 +1529,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3045,6 +1551,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3066,6 +1573,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3087,6 +1595,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3108,6 +1617,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3129,6 +1639,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3150,6 +1661,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3171,6 +1683,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3192,6 +1705,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3213,6 +1727,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3234,6 +1749,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3248,26 +1764,6 @@ "integrity": "sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==", "license": "MIT" }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-react": { - "version": "0.546.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.546.0.tgz", - "integrity": "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ==", - "dev": true, - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/lz4js": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/lz4js/-/lz4js-0.2.0.tgz", @@ -3284,75 +1780,6 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mitt": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", @@ -3433,15 +1860,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -3477,28 +1895,6 @@ "node-gyp-build-optional-packages-test": "build-test.js" } }, - "node_modules/node-releases": { - "version": "2.0.52", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.52.tgz", - "integrity": "sha512-MRlTqhAfoMx/4mhEbPo3Hi02g9LJZaJkka69V6h67Cb1gjrAG0jsTE4CZX1eptNx+VCAwJmfpnDIF4P0Nh1A7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/obug": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", @@ -3513,18 +1909,6 @@ "node": ">=12.20.0" } }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/p-timeout": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-4.1.0.tgz", @@ -3534,21 +1918,6 @@ "node": ">=10" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", - "license": "MIT" - }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -3605,19 +1974,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/proxy-agent-negotiate": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-agent-negotiate/-/proxy-agent-negotiate-1.1.0.tgz", @@ -3652,79 +2008,6 @@ "node": ">=22.12.0" } }, - "node_modules/qs": { - "version": "6.15.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", - "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", - "license": "BSD-3-Clause", - "dependencies": { - "es-define-property": "^1.0.1", - "side-channel": "^1.1.1" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/react": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", - "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", - "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.8" - } - }, - "node_modules/react-refresh": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", - "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/rollup": { "version": "4.62.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", @@ -3771,26 +2054,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/safe-compare": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/safe-compare/-/safe-compare-1.1.4.tgz", @@ -3800,12 +2063,6 @@ "buffer-alloc": "^1.2.0" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, "node_modules/sandwich-stream": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/sandwich-stream/-/sandwich-stream-2.0.2.tgz", @@ -3815,155 +2072,6 @@ "node": ">= 0.10" } }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/side-channel": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4", - "side-channel-list": "^1.0.1", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -4026,15 +2134,6 @@ "dev": true, "license": "MIT" }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/std-env": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", @@ -4073,27 +2172,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/tailwindcss": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", - "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", - "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/telegraf": { "version": "4.16.3", "resolved": "https://registry.npmjs.org/telegraf/-/telegraf-4.16.3.tgz", @@ -4160,15 +2238,6 @@ "node": ">=14.0.0" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -4678,19 +2747,6 @@ "@esbuild/win32-x64": "0.28.1" } }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/typed-query-selector": { "version": "2.12.2", "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", @@ -4727,64 +2783,6 @@ "dev": true, "license": "MIT" }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/vite": { "version": "6.4.3", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", @@ -5053,13 +3051,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, "node_modules/yargs": { "version": "18.1.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz", diff --git a/package.json b/package.json index dce35ae..74a26c9 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,19 @@ { "name": "telemax", "private": true, - "version": "0.3.9", + "version": "0.4.0", "type": "module", "scripts": { "dev": "tsx watch src/server/app.ts", - "build": "vite build && esbuild src/server/app.ts --bundle --platform=node --format=esm --packages=external --sourcemap --outfile=dist/server.mjs", + "build": "esbuild src/server/app.ts --bundle --platform=node --format=esm --packages=external --sourcemap --outfile=dist/server.mjs", "start": "NODE_ENV=production node dist/server.mjs", - "lint": "tsc --noEmit && tsc --noEmit -p tsconfig.web.json", + "lint": "tsc --noEmit", "test": "vitest run", "test:watch": "vitest", "auth:cli": "tsx scripts/dev/auth-cli.ts" }, "dependencies": { "dotenv": "^17.2.3", - "express": "^4.21.2", "fzstd": "^0.1.1", "https-proxy-agent": "^9.1.0", "lottie-web": "^5.13.0", @@ -23,25 +22,13 @@ "puppeteer-core": "^25.6.0", "socks-proxy-agent": "^10.1.0", "telegraf": "^4.16.3", - "undici": "^8.10.0", - "ws": "^8.21.1" + "undici": "^8.10.0" }, "devDependencies": { - "@tailwindcss/vite": "^4.1.14", - "@types/express": "^4.17.21", "@types/node": "^22.20.1", - "@types/react": "^19.2.18", - "@types/react-dom": "^19.2.4", - "@types/ws": "^8.18.1", - "@vitejs/plugin-react": "^5.0.4", "esbuild": "^0.25.0", - "lucide-react": "^0.546.0", - "react": "^19.0.1", - "react-dom": "^19.0.1", - "tailwindcss": "^4.1.14", "tsx": "^4.21.0", "typescript": "~5.8.2", - "vite": "^6.2.3", "vitest": "^4.1.10" } } diff --git a/scripts/release.sh b/scripts/release.sh index ad813af..dd5ac2c 100644 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -30,10 +30,26 @@ echo ">> Releasing ${TAG}" # The tag must already exist (coordinate the tag first, then run this). if ! git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then - echo "!! Tag ${TAG} not found. Create it first: git tag ${TAG} then re-run." >&2 + echo "!! Tag ${TAG} not found. Create it first: git tag -s ${TAG} then re-run." >&2 exit 1 fi +# Signed-release trust (v0.4): once the maintainer's public key is pinned in the repo +# (release-signing-key.asc), refuse to publish a tag that isn't validly signed by it — so neither +# GitHub nor the mirror can ever carry code that update.sh's verify-tag would (correctly) reject. +# Create signed tags with: git tag -s vX.Y.Z (needs the private key + gpg on this machine). +if [ -f release-signing-key.asc ]; then + VERIFY_HOME=$(mktemp -d) + GNUPGHOME="$VERIFY_HOME" gpg --quiet --import release-signing-key.asc >/dev/null 2>&1 || true + if ! GNUPGHOME="$VERIFY_HOME" git -c gpg.program=gpg verify-tag "${TAG}" >/dev/null 2>&1; then + rm -rf "$VERIFY_HOME" + echo "!! Tag ${TAG} is not validly signed by the pinned release key. Sign it: git tag -s -f ${TAG}" >&2 + exit 1 + fi + rm -rf "$VERIFY_HOME" + echo ">> release signature OK: ${TAG}" +fi + echo ">> [1/4] push to GitHub (origin)" git push origin main git push origin "${TAG}" diff --git a/setup.sh b/setup.sh index c2ecf9f..f562783 100755 --- a/setup.sh +++ b/setup.sh @@ -1,113 +1,14 @@ #!/usr/bin/env bash -# Interactive first-time setup for Telemax. Generates the two secret keys -# automatically, asks only for the two things nobody but you can provide -# (the Telegram bot token and target group id), writes .env, and offers to -# build + start the bridge right away. +# Interactive first-time setup for Telemax (v0.4 — headless, no web panel). +# Generates the session-encryption key automatically, asks only for the two things +# nobody but you can provide (the Telegram bot token and target group), lets you pick +# the image size (animated stickers on/off), writes .env, and builds + starts the +# bridge. MAX authorization happens afterwards IN THE BOT: send /login to it in a DM. set -euo pipefail cd "$(dirname "$0")" bold() { printf '\033[1m%s\033[0m\n' "$1"; } -# Waits until the container's HTTPS panel answers /health. MAX connects a moment -# after the container starts, so /auth/* would 503 if we asked too early. Expects -# API_URL set. Panel uses a self-signed cert generated on first start — hence -k. -wait_for_server() { - echo "Жду готовности сервера..." - for attempt in 1 2 3 4 5 6 7 8 9 10; do - curl -sk --max-time 2 "$API_URL/health" >/dev/null 2>&1 && break - sleep 2 - done -} - -# Interactive MAX auth (phone -> SMS -> optional cloud password) against the running -# container's /api/auth/*, the same endpoints the web panel uses. Runs both on a -# fresh install and on the re-auth path below (an already-configured install where -# auth was never finished). Expects API_URL, API_KEY, HOST, PORT set; sets AUTHED=1 -# on success. -run_max_auth() { - echo - echo "════════════════════════════════════════════════════════════════" - bold " ШАГ АВТОРИЗАЦИИ MAX — БЕЗ НЕГО МОСТ НЕ ЗАРАБОТАЕТ" - echo "════════════════════════════════════════════════════════════════" - echo "Введите номер MAX и код из SMS. Если пропустить (просто Enter) —" - echo "контейнер останется запущенным, но НЕ подключённым к аккаунту, пока" - echo "вы не авторизуетесь позже через веб-панель." - echo - AUTHED="" - read -rp "Номер телефона MAX (с кодом страны, напр. +79991234567), или Enter чтобы позже: " MAX_PHONE - if [ -n "$MAX_PHONE" ]; then - PHONE_OK="" - for attempt in 1 2 3; do - PHONE_RESP=$(curl -sk -X POST -H "x-api-key: $API_KEY" -H "Content-Type: application/json" \ - -d "{\"phone\":\"$MAX_PHONE\"}" "$API_URL/auth/phone") - if echo "$PHONE_RESP" | grep -q '"success":true'; then - PHONE_OK=1 - break - fi - sleep 2 - done - if [ -n "$PHONE_OK" ]; then - echo "Код отправлен на $MAX_PHONE." - read -rp "Код из SMS: " MAX_CODE - VERIFY_RESP=$(curl -sk -X POST -H "x-api-key: $API_KEY" -H "Content-Type: application/json" \ - -d "{\"code\":\"$MAX_CODE\"}" "$API_URL/auth/verify") - if echo "$VERIFY_RESP" | grep -q '"passwordRequired":true'; then - # Some MAX accounts have a password set as a second factor on top of SMS. - # A wrong password can be retried freely — the auth session behind it - # doesn't expire until a correct one goes through (confirmed live 2026-08-14). - HINT=$(echo "$VERIFY_RESP" | grep -o '"hint":"[^"]*"' | sed 's/"hint":"//;s/"$//') - echo "Этот MAX-аккаунт защищён паролем (второй фактор поверх SMS)." - [ -n "$HINT" ] && echo "Подсказка: $HINT" - PASSWORD_OK="" - while [ -z "$PASSWORD_OK" ]; do - read -rsp "Пароль: " MAX_PASSWORD - echo - PASSWORD_RESP=$(curl -sk -X POST -H "x-api-key: $API_KEY" -H "Content-Type: application/json" \ - -d "{\"password\":\"$MAX_PASSWORD\"}" "$API_URL/auth/password") - if echo "$PASSWORD_RESP" | grep -q '"success":true'; then - PASSWORD_OK=1 - AUTHED=1 - bold "Готово — мост авторизован и подключён к MAX." - else - echo "❌ Неверный пароль, попробуйте ещё раз (или Ctrl+C — тогда через веб-панель: https://$HOST:$PORT)." - fi - done - elif echo "$VERIFY_RESP" | grep -q '"success":true'; then - AUTHED=1 - bold "Готово — мост авторизован и подключён к MAX." - else - echo "❌ Не удалось подтвердить код: $VERIFY_RESP" - echo "Попробуйте ещё раз через веб-панель: https://$HOST:$PORT" - fi - else - echo "❌ Не удалось запросить SMS: $PHONE_RESP" - echo "Попробуйте через веб-панель: https://$HOST:$PORT" - fi - else - echo "Ок — авторизацию можно завершить позже через веб-панель (см. ниже)." - fi -} - -# Big, unmissable closing summary — authed vs not, plus the panel URL and key. -# Expects AUTHED, HOST, PORT, API_KEY. -print_final_status() { - echo - echo "════════════════════════════════════════════════════════════════" - if [ -n "$AUTHED" ]; then - bold " ✅ ГОТОВО. Мост авторизован, запущен и подключён к MAX." - else - bold " ⚠️ МОСТ ЗАПУЩЕН, НО MAX ПОКА НЕ АВТОРИЗОВАН" - echo " Без авторизации сообщения не будут пересылаться. Завершите её:" - echo " откройте https://$HOST:$PORT, введите ключ ниже, затем номер и код из SMS." - fi - echo "════════════════════════════════════════════════════════════════" - echo " Веб-панель: https://$HOST:$PORT" - echo " Ключ входа: $API_KEY" - echo " (сертификат самоподписанный — браузер предупредит один раз:" - echo " «Дополнительно» → «Перейти на сайт». Ключ можно вернуть командой /apikey у бота.)" - echo "════════════════════════════════════════════════════════════════" -} - # Runs on every invocation, even on an already-configured install (the .env # check below exits before the rest of setup) — an update pulled in via the # watcher itself needs the watcher already installed to have gotten here, and @@ -149,60 +50,28 @@ else echo "Пропускаю установку вотчера обновлений — нужны root и systemd. Обновляться придётся вручную: ./update.sh" fi +# Already configured — nothing to re-ask. If MAX isn't authorized yet, that's now +# a one-liner in the bot (/login), so we don't need the old console-auth flow. if [ -f .env ]; then - # Already configured — but the MAX auth step might never have been finished - # (interrupted mid-flow, or only the bot/group part was done, or the account - # has a cloud password and the person bailed at that prompt). Re-running the - # installer used to just say "нечего делать" and exit, leaving the web panel as - # the only recovery path. Instead: check whether MAX is actually authorized and, - # if not, offer to finish it right here in the console. - set -a - # shellcheck disable=SC1091 - . ./.env 2>/dev/null || true - set +a - PORT="${PORT:-3000}" - API_URL="https://localhost:$PORT/api" - PUBLIC_IP=$(curl -s --max-time 3 ifconfig.me || true) - HOST="${PUBLIC_IP:-<адрес-сервера>}" - if ! command -v docker >/dev/null 2>&1; then - echo ".env уже существует, но Docker недоступен на этой машине —" - echo "запустите контейнер там, где есть Docker, и авторизуйтесь через веб-панель." + echo ".env уже существует, но Docker недоступен на этой машине — запустите контейнер там, где есть Docker." exit 0 fi - if [ -z "$(docker compose ps --status running --format '{{.Name}}' 2>/dev/null)" ]; then read -rp ".env есть, но контейнер не запущен. Запустить сейчас? [Y/n] " RUN_NOW if [ "${RUN_NOW:-Y}" = "n" ] || [ "${RUN_NOW:-Y}" = "N" ]; then - echo "Ок. Когда будете готовы: docker compose up -d, затем ./setup.sh или веб-панель." + echo "Ок. Когда будете готовы: docker compose up -d" exit 0 fi GIT_COMMIT=$(git rev-parse HEAD 2>/dev/null || echo unknown) docker compose up -d --build fi - - wait_for_server - # /api/status returns the active phone once MAX is authorized; empty until then. - STATUS=$(curl -sk --max-time 3 -H "x-api-key: ${API_KEY:-}" "$API_URL/status" 2>/dev/null || true) - if echo "$STATUS" | grep -q '"phone":"[^"]'; then - echo "✅ Уже настроено и авторизовано в MAX. Ничего делать не нужно." - echo " Веб-панель: https://$HOST:$PORT" - exit 0 - fi - - echo - bold "⚠️ .env есть, но MAX ещё не авторизован — шаг авторизации не завершён." - read -rp "Пройти авторизацию сейчас? [Y/n] " DO_AUTH - if [ "${DO_AUTH:-Y}" = "n" ] || [ "${DO_AUTH:-Y}" = "N" ]; then - echo "Ок — можно позже через веб-панель: https://$HOST:$PORT (ключ: /apikey у бота или grep API_KEY .env)." - exit 0 - fi - run_max_auth - print_final_status + echo "✅ Уже настроено и запущено." + echo " Если MAX ещё не авторизован — напишите боту в ЛИЧКУ: /login (или в группе: /panel → «🔐 Вход в MAX»)." exit 0 fi if ! command -v openssl >/dev/null 2>&1; then - echo "Не найден openssl — он нужен, чтобы сгенерировать ключи. Установите его (обычно уже есть на Ubuntu/Debian: apt install openssl) и запустите скрипт снова." + echo "Не найден openssl — он нужен для генерации ключа шифрования сессии. Установите (apt install openssl) и запустите скрипт снова." exit 1 fi @@ -244,18 +113,6 @@ check_tcp() { return 1 } -# Starting from $1, returns the first port nothing is already listening on -# locally. Used instead of hard-failing when 3000 is taken (e.g. a leftover -# container from an earlier install attempt) — just use the next one free. -find_free_port() { - local port=$1 - while (exec 3<>"/dev/tcp/127.0.0.1/$port") 2>/dev/null; do - exec 3>&- 3<&- - port=$((port + 1)) - done - echo "$port" -} - # Ask about a Telegram proxy BEFORE the reachability check: on a host where Telegram # is reachable only through a proxy (e.g. a home server with no direct route to # api.telegram.org), a direct check would falsely fail and abort setup. MAX always @@ -272,21 +129,23 @@ echo echo "Проверяю связь с серверами MAX и Telegram..." # MAX is fatal: it's always direct, so nothing the user could re-type fixes a blocked -# MAX — a firewall/geo-block is an environment problem, not a typo. -if check_tcp 155.212.204.150 443; then - echo " MAX (155.212.204.150:443): OK" +# MAX — a firewall/geo-block is an environment problem, not a typo. Checked by NAME so +# an IPv6-only host resolves via DNS64/NAT64 (MAX itself is IPv4-only). +if check_tcp api2.oneme.ru 443; then + echo " MAX (api2.oneme.ru:443): OK" else - echo " MAX (155.212.204.150:443): нет связи" + echo " MAX (api2.oneme.ru:443): нет связи" echo echo "Без связи с сервером MAX мост работать не сможет — обычно это файрвол хостинга" - echo "или гео-блокировка. Проверьте сеть сервера и запустите setup.sh снова." + echo "или гео-блокировка. На IPv6-only хосте нужен NAT64/DNS64 у провайдера. Проверьте" + echo "сеть сервера и запустите setup.sh снова." exit 1 fi # Telegram is NOT fatal: the usual cause is a mistyped proxy (or a host that needs a # proxy at all) — both fixable right here. So loop and let the user re-enter the proxy # and re-check instead of aborting, with an explicit "skip" escape so a genuinely -# blocked host isn't a dead end (the proxy can still be set later in the panel). +# blocked host isn't a dead end (the proxy can still be set later in .env). while true; do TG_PROXY_ARGS=() if [ -n "$TELEGRAM_PROXY" ]; then @@ -305,11 +164,11 @@ while true; do fi echo " • впишите прокси (socks5://… или http://…) и Enter — перепроверю через него;" echo " • пустой Enter — перепроверить напрямую, без прокси;" - echo " • skip — продолжить установку без проверки (прокси можно задать позже в панели)." + echo " • skip — продолжить установку без проверки (прокси можно задать позже в .env)." read -rp " > " TG_INPUT if [ "$TG_INPUT" = "skip" ]; then echo " Пропускаю проверку Telegram. Мост поднимется, но без связи с Telegram пересылки" - echo " не будет — задайте прокси в веб-панели (Configuration → Telegram-прокси)." + echo " не будет — задайте TELEGRAM_PROXY в .env и пересоберите." break fi TELEGRAM_PROXY="$TG_INPUT" @@ -403,37 +262,45 @@ if [ -f "$AVATAR" ]; then fi fi -API_KEY=$(openssl rand -hex 24) MAX_SESSION_KEY=$(openssl rand -hex 32) -PORT=$(find_free_port 3000) -if [ "$PORT" != "3000" ]; then - echo "Порт 3000 занят — использую $PORT вместо него." +# Animated stickers (.tgs) — the one real image-size knob. Rendering them to playable +# video needs a headless Chromium + ffmpeg (~1.4 GB); opting out ("slim") relays them as +# a static picture instead and keeps the image ~0.3 GB with a much faster build. 30-second +# timeout so an unattended install isn't blocked — defaults to full. +echo +echo "Анимированные стикеры (.tgs):" +echo " • FULL (по умолчанию) — проигрываются как видео. Образ +~1.4 ГБ (Chromium+ffmpeg), сборка дольше." +echo " • SLIM — уходят статической картинкой. Образ ~0.3 ГБ, быстрая сборка." +STICKERS=full +if read -t 30 -rp "Впишите slim для лёгкого образа, или Enter (жду 30с) — оставить full: " STICKERS_CHOICE; then + case "${STICKERS_CHOICE:-}" in + slim | SLIM | s | S) STICKERS=slim ;; + *) STICKERS=full ;; + esac +else + echo + echo " (30с прошло — оставляю full)" fi +echo " → образ: $STICKERS" -# .env carries every secret this install has (API key, session-encryption key, -# bot token) — don't leave it readable to other local users. +# .env carries the session-encryption key and bot token — don't leave it readable to +# other local users. STICKERS is read by docker-compose.yml as a build arg. umask 077 cat > .env </dev/null 2>&1; then - echo "Docker не найден на этой машине — установите Docker и Docker Compose, затем запустите:" - echo " docker compose up -d --build" + echo "Docker не найден — установите Docker и Docker Compose, затем: docker compose up -d --build" exit 0 fi @@ -444,18 +311,18 @@ if [ "${RUN_NOW:-Y}" = "n" ] || [ "${RUN_NOW:-Y}" = "N" ]; then fi echo -echo "Собираю образ — первая сборка дольше обычного (внутри headless-браузер для рендера стикеров), обычно несколько минут." +if [ "$STICKERS" = "full" ]; then + echo "Собираю образ (full) — первая сборка дольше (внутри headless-браузер для стикеров), обычно несколько минут." +else + echo "Собираю образ (slim) — быстрая сборка." +fi GIT_COMMIT=$(git rev-parse HEAD 2>/dev/null || echo unknown) docker compose up -d --build -PUBLIC_IP=$(curl -s --max-time 3 ifconfig.me || true) -HOST="${PUBLIC_IP:-<адрес-сервера>}" - echo -bold "Контейнер собран и запущен. Остался ОДИН обязательный шаг ниже." - -# Авторизация в MAX прямо здесь, без переключения в браузер — тот же /api/auth/*, -# которым пользуется веб-панель, просто из консоли. -API_URL="https://localhost:$PORT/api" -wait_for_server -run_max_auth -print_final_status +echo "════════════════════════════════════════════════════════════════" +bold " ✅ Мост собран и запущен. Остался ОДИН обязательный шаг: авторизация MAX." +echo " Откройте вашего бота в Telegram и напишите ему в ЛИЧКУ команду:" +bold " /login" +echo " Введёте номер MAX и код из SMS прямо в личке — в группу они не попадут." +echo " (Альтернатива: в группе /panel → «🔐 Вход в MAX».)" +echo "════════════════════════════════════════════════════════════════" diff --git a/src/App.tsx b/src/App.tsx deleted file mode 100644 index 67b6487..0000000 --- a/src/App.tsx +++ /dev/null @@ -1,674 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import { Activity, KeyRound, MessageSquare, Server, Settings, Shield } from 'lucide-react'; -import { apiFetch, clearStoredApiKey, getStoredApiKey, openStatusSocket, setStoredApiKey } from './api'; - -interface LogEntry { - time: string; - opcode: string; - dir: string; - size: string; - payload: string; -} - -interface StatusData { - max: boolean; - tg: boolean; - deviceId: string; - phone: string; - latencyMs: number | null; -} - -interface Metrics { - packetsSent: number; - packetsReceived: number; - uptimeSeconds: number; -} - -interface ChatMapping { - maxChatId: string; // normalized to a decimal string server-side (BigInt-safe) - telegramTopicId: number; - title?: string; - createdAt: string; -} - -interface MaxChat { - id: number; - type: string; - displayName: string; - lastMessage?: { text?: string; sender?: number }; -} - -function formatUptime(seconds: number): string { - const h = Math.floor(seconds / 3600); - const m = Math.floor((seconds % 3600) / 60); - return `${h}h ${m}m`; -} - -// Human-readable current proxy mode for the "Сейчас:" line, with credentials stripped. -function proxyDisplay(url: string): string { - if (!url) return 'напрямую (без прокси)'; - try { - const u = new URL(url); - u.username = ''; - u.password = ''; - return `через прокси ${u.toString()}`; - } catch { - return `через прокси ${url}`; - } -} - -function ApiKeyGate({ onSubmit, error }: { onSubmit: (key: string) => void; error: string }) { - const [value, setValue] = useState(''); - return ( -
-
-

- API Key -

-

- Ключ печатается в консоли при установке (setup.sh). Если потеряли — он лежит в файле .env в папке установки: grep API_KEY .env. Либо спросите бота командой /apikey в вашей Telegram-группе. -

- {error &&
{error}
} - setValue(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && onSubmit(value)} - className="w-full bg-[#0c0d0f] border border-white/10 rounded px-4 py-3 text-sm text-white mb-4 focus:outline-none focus:border-blue-500" - placeholder="x-api-key" - /> - -
-
- ); -} - -export default function App() { - // Lets the bot's /apikey link (http://host:port/?key=...) log you straight - // in instead of just pointing at the login form — store it like a normal - // login and strip it from the URL bar right away so it doesn't linger in - // history/bookmarks. - const [apiKey, setApiKey] = useState(() => { - const fromUrl = new URLSearchParams(window.location.search).get('key'); - if (fromUrl) { - setStoredApiKey(fromUrl); - window.history.replaceState({}, '', window.location.pathname); - return fromUrl; - } - return getStoredApiKey(); - }); - const [gateError, setGateError] = useState(''); - const [logs, setLogs] = useState([]); - const [status, setStatus] = useState({ max: false, tg: false, deviceId: '', phone: '', latencyMs: null }); - const [metrics, setMetrics] = useState({ packetsSent: 0, packetsReceived: 0, uptimeSeconds: 0 }); - const [chatMappings, setChatMappings] = useState([]); - const [maxChats, setMaxChats] = useState([]); - const [sendChatId, setSendChatId] = useState(''); - const [sendText, setSendText] = useState(''); - const [sendStatus, setSendStatus] = useState(''); - const [activeTab, setActiveTab] = useState('logs'); - const [phone, setPhone] = useState(''); - const [code, setCode] = useState(''); - const [password, setPassword] = useState(''); - const [passwordHint, setPasswordHint] = useState(''); - const [authStep, setAuthStep] = useState<'phone' | 'code' | 'password' | 'done'>('phone'); - const [authError, setAuthError] = useState(''); - const [proxyUrl, setProxyUrl] = useState(''); - const [proxyActive, setProxyActive] = useState(''); - const [proxyStatus, setProxyStatus] = useState(''); - const [proxyBusy, setProxyBusy] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const logsEndRef = useRef(null); - const isSubmitting = useRef(false); - - function handleUnauthorized() { - clearStoredApiKey(); - setApiKey(null); - setGateError('Неверный или отозванный API key.'); - } - - function handleGateSubmit(key: string) { - if (!key.trim()) return; - setStoredApiKey(key.trim()); - setApiKey(key.trim()); - setGateError(''); - } - - const handleForceStop = async () => { - await apiFetch('/api/system/stop', { method: 'POST' }).catch(console.error); - }; - const handleForceStart = async () => { - await apiFetch('/api/system/start', { method: 'POST' }).catch(console.error); - }; - - const refreshChats = () => { - apiFetch('/api/chats') - .then((r: Response) => (r.ok ? r.json() : { data: { chats: [] } })) - .then((body: { data?: { chats?: MaxChat[] } }) => setMaxChats(body.data?.chats ?? [])) - .catch(() => {}); - }; - - const handleSendMessage = async () => { - if (!sendChatId || !sendText) return; - setSendStatus('Sending...'); - try { - const res = await apiFetch(`/api/chats/${sendChatId}/messages`, { method: 'POST', body: JSON.stringify({ text: sendText }) }); - if (res.status === 401) return handleUnauthorized(); - const data = await res.json().catch(() => ({})); - setSendStatus(res.ok ? `Sent (cid ${data.cid})` : data.error || 'Failed'); - if (res.ok) { - setSendText(''); - // the server only learns the send succeeded once MAX echoes a confirmation frame back, - // which lands shortly after this request returns — give it a beat before refetching - setTimeout(refreshChats, 700); - } - } catch { - setSendStatus('Failed'); - } - }; - - const handleRequestSms = async () => { - if (isLoading || isSubmitting.current) return; - isSubmitting.current = true; - setIsLoading(true); - setAuthError(''); - try { - const res = await apiFetch('/api/auth/phone', { method: 'POST', body: JSON.stringify({ phone }) }); - if (res.status === 401) return handleUnauthorized(); - const data = await res.json().catch(() => ({})); - if (!res.ok || data.error) setAuthError(data.error || 'Failed to request SMS'); - else setAuthStep('code'); - } finally { - setIsLoading(false); - isSubmitting.current = false; - } - }; - - const handleVerifyCode = async () => { - if (isLoading || isSubmitting.current) return; - isSubmitting.current = true; - setIsLoading(true); - setAuthError(''); - try { - const res = await apiFetch('/api/auth/verify', { method: 'POST', body: JSON.stringify({ code }) }); - if (res.status === 401) return handleUnauthorized(); - const data = await res.json().catch(() => ({})); - if (!res.ok || data.error) setAuthError(data.error || 'Failed to verify code'); - else if (data.passwordRequired) { - setPasswordHint(data.hint || ''); - setAuthStep('password'); - } else setAuthStep('done'); - } finally { - setIsLoading(false); - isSubmitting.current = false; - } - }; - - // Only reached for password-protected MAX accounts — verifyCode above - // returned passwordRequired instead of completing the login directly. A - // wrong password can be retried freely (the server keeps the same - // trackId), so this deliberately doesn't fall back to the code step. - const handleVerifyPassword = async () => { - if (isLoading || isSubmitting.current) return; - isSubmitting.current = true; - setIsLoading(true); - setAuthError(''); - try { - const res = await apiFetch('/api/auth/password', { method: 'POST', body: JSON.stringify({ password }) }); - if (res.status === 401) return handleUnauthorized(); - const data = await res.json().catch(() => ({})); - if (!res.ok || data.error) setAuthError(data.error || 'Failed to verify password'); - else setAuthStep('done'); - } finally { - setIsLoading(false); - isSubmitting.current = false; - } - }; - - // Disconnects MAX and deletes the encrypted session, same as /kill's MAX-side - // teardown — but Telegram topics/history are untouched, so this is safe as a - // plain "log out and reauthorize" action rather than something destructive. - const handleLogout = async () => { - if (isLoading || isSubmitting.current) return; - if (!window.confirm('Разлогинить MAX-сессию? Понадобится новая SMS-авторизация. История переписки в Telegram не затрагивается.')) return; - isSubmitting.current = true; - setIsLoading(true); - try { - const res = await apiFetch('/api/auth/logout', { method: 'POST' }); - if (res.status === 401) return handleUnauthorized(); - setPhone(''); - setCode(''); - setPassword(''); - setPasswordHint(''); - setAuthError(''); - setAuthStep('phone'); - } finally { - setIsLoading(false); - isSubmitting.current = false; - } - }; - - // Telegram proxy: test a value live before committing it. MAX always stays direct; - // saving persists to ./data and restarts the process to apply (Telegraf binds its - // agent at construction), and compose's restart policy brings the container back. - const handleTestProxy = async () => { - setProxyBusy(true); - setProxyStatus('Проверяю…'); - try { - const res = await apiFetch('/api/proxy/test', { method: 'POST', body: JSON.stringify({ proxy: proxyUrl }) }); - if (res.status === 401) return handleUnauthorized(); - const data = await res.json().catch(() => ({})); - setProxyStatus(data.ok ? '✅ Прокси доступен' : `❌ ${data.error || 'нет связи'}`); - } catch { - setProxyStatus('❌ ошибка проверки'); - } finally { - setProxyBusy(false); - } - }; - - const handleSaveProxyRestart = async () => { - if (!window.confirm('Сохранить прокси и перезапустить мост? MAX останется напрямую. Перерыв в работе — несколько секунд.')) return; - setProxyBusy(true); - setProxyStatus('Сохраняю…'); - try { - const res = await apiFetch('/api/proxy', { method: 'POST', body: JSON.stringify({ proxy: proxyUrl }) }); - if (res.status === 401) return handleUnauthorized(); - const data = await res.json().catch(() => ({})); - if (!res.ok || data.error) { - setProxyStatus(`❌ ${data.error || 'не сохранено'}`); - return; - } - setProxyStatus('Сохранено. Перезапускаю…'); - setProxyActive(proxyUrl.trim()); // reflect the new mode immediately; restart applies it - await apiFetch('/api/system/restart', { method: 'POST' }).catch(() => {}); - } finally { - setProxyBusy(false); - } - }; - - useEffect(() => { - if (!apiKey) return; - - let cancelled = false; - - const refreshStatus = (isInitial: boolean) => { - apiFetch('/api/status') - .then((r: Response) => { - if (r.status === 401) throw new Error('unauthorized'); - return r.json(); - }) - .then((data: { packetsSent: number; packetsReceived: number; uptimeSeconds: number; maxOnline: boolean; tgActive: boolean; deviceId: string; phone: string; latencyMs: number | null }) => { - if (cancelled) return; - setMetrics({ packetsSent: data.packetsSent, packetsReceived: data.packetsReceived, uptimeSeconds: data.uptimeSeconds }); - setStatus({ max: data.maxOnline, tg: data.tgActive, deviceId: data.deviceId, phone: data.phone, latencyMs: data.latencyMs }); - if (isInitial && data.phone) setAuthStep('done'); - }) - .catch((err: Error) => { - if (!cancelled && err.message === 'unauthorized') handleUnauthorized(); - }); - }; - - refreshStatus(true); - // The WS pushes status changes, but packets/uptime only come from /api/status — - // without polling they froze at their on-mount values until a full page reload. - const metricsTimer = window.setInterval(() => refreshStatus(false), 30_000); - - apiFetch('/api/chat-mappings') - .then((r: Response) => (r.ok ? r.json() : { data: [] })) - .then((body: { data?: ChatMapping[] }) => !cancelled && setChatMappings(body.data ?? [])) - .catch(() => {}); - - apiFetch('/api/chats') - .then((r: Response) => (r.ok ? r.json() : { data: { chats: [] } })) - .then((body: { data?: { chats?: MaxChat[] } }) => !cancelled && setMaxChats(body.data?.chats ?? [])) - .catch(() => {}); - - apiFetch('/api/proxy') - .then((r: Response) => (r.ok ? r.json() : { proxy: '' })) - .then((body: { proxy?: string }) => { - if (cancelled) return; - setProxyUrl(body.proxy ?? ''); - setProxyActive(body.proxy ?? ''); - }) - .catch(() => {}); - - // Reconnect with a small delay when the socket drops (server redeploy/restart) — - // otherwise the panel silently froze until a manual reload. - let ws: WebSocket | null = null; - let reconnectTimer: number | undefined; - const connect = () => { - ws = openStatusSocket(); - ws.onmessage = (event: MessageEvent) => { - const msg = JSON.parse(event.data); - if (msg.type === 'init_logs') setLogs(msg.data); - else if (msg.type === 'log') setLogs((prev) => [...prev, msg.data].slice(-50)); - else if (msg.type === 'status') setStatus(msg.data); - }; - ws.onclose = () => { - if (!cancelled) reconnectTimer = window.setTimeout(connect, 3_000); - }; - }; - connect(); - - return () => { - cancelled = true; - window.clearInterval(metricsTimer); - window.clearTimeout(reconnectTimer); - ws?.close(); - }; - }, [apiKey]); - - useEffect(() => { - logsEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [logs]); - - if (!apiKey) return ; - - return ( -
-
-
-
- M -
-
-

MAX Bridge

-

Real-time Protocol Proxy & Sync

-
-
-
-
-
- MAX TCP: {status.max ? 'Online' : 'Offline'} -
-
-
- TG Bot: {status.tg ? 'Active' : 'Offline'} -
- {status.max ? ( - - ) : ( - - )} -
-
- -
- - -
-
- - - - -
- -
- {activeTab === 'logs' && ( -
-
-
Timestamp
-
OpCode
-
Dir
-
Length
-
Payload (masked)
-
-
- {logs.length === 0 &&
Waiting for traffic...
} - {logs.map((log, i) => ( -
-
{log.time}
-
{log.opcode}
-
- {log.dir} -
-
{log.size}
-
{log.payload}
-
- ))} -
-
-
- )} - - {activeTab === 'config' && ( -
-
-

- MAX Authentication -

- {authError &&
{authError}
} - {authStep === 'phone' && ( -
- setPhone(e.target.value)} - className="w-full bg-[#0c0d0f] border border-white/10 rounded px-4 py-3 text-sm text-white" - /> - -
- )} - {authStep === 'code' && ( -
- setCode(e.target.value)} - className="w-full bg-[#0c0d0f] border border-white/10 rounded px-4 py-3 text-sm text-white font-mono tracking-widest" - /> -
- - -
-
- )} - {authStep === 'password' && ( -
-

Этот MAX-аккаунт защищён паролем (второй фактор поверх SMS).

- {passwordHint &&

Подсказка: {passwordHint}

} - setPassword(e.target.value)} - className="w-full bg-[#0c0d0f] border border-white/10 rounded px-4 py-3 text-sm text-white" - /> - -
- )} - {authStep === 'done' && ( -
-
- -
-

Authenticated as {status.phone}

- -
- )} -
- -
-

- Telegram-прокси -

-

- Если сервер выходит в Telegram только через прокси. MAX всегда идёт напрямую. - Форматы: socks5://… или{' '} - http://… (можно с логином:паролем@). - Применяется после перезапуска. -

-
- Сейчас: - {proxyDisplay(proxyActive)} -
- setProxyUrl(e.target.value)} - className="w-full bg-[#0c0d0f] border border-white/10 rounded px-4 py-3 text-sm text-white font-mono" - /> -

Пусто = напрямую, без прокси

- {proxyStatus &&
{proxyStatus}
} -
- - -
-
-
- )} - - {activeTab === 'chats' && ( -
-
- setSendChatId(e.target.value)} - className="w-32 bg-[#0c0d0f] border border-white/10 rounded px-3 py-2 text-sm text-white font-mono" - /> - setSendText(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()} - className="flex-1 bg-[#0c0d0f] border border-white/10 rounded px-3 py-2 text-sm text-white" - /> - - {sendStatus && {sendStatus}} -
-
-
-
Name
-
chatId
-
Type
-
Last message
-
-
- {maxChats.length === 0 &&
No chats (or not authenticated yet).
} - {maxChats.map((c) => ( -
setSendChatId(String(c.id))}> -
{c.displayName}
-
{c.id}
-
{c.type}
-
{c.lastMessage?.text ?? '—'}
-
- ))} -
-
-
- )} - - {activeTab === 'mapping' && ( -
-
-
MAX chatId
-
TG topicId
-
Title
-
Created
-
-
- {chatMappings.length === 0 &&
No chat mappings yet — created automatically on first message.
} - {chatMappings.map((m) => ( -
-
{m.maxChatId}
-
{m.telegramTopicId}
-
{m.title ?? '—'}
-
{m.createdAt}
-
- ))} -
-
- )} - -
-
-
Packets S/R
-
{metrics.packetsSent} / {metrics.packetsReceived}
-
-
-
Uptime
-
{formatUptime(metrics.uptimeSeconds)}
-
-
-
Latency
-
{status.latencyMs != null ? `${status.latencyMs}ms` : '—'}
-
-
-
-
-
-
- ); -} diff --git a/src/api.ts b/src/api.ts deleted file mode 100644 index b901149..0000000 --- a/src/api.ts +++ /dev/null @@ -1,28 +0,0 @@ -const STORAGE_KEY = 'telemax_api_key'; - -export function getStoredApiKey(): string | null { - return localStorage.getItem(STORAGE_KEY); -} - -export function setStoredApiKey(key: string): void { - localStorage.setItem(STORAGE_KEY, key); -} - -export function clearStoredApiKey(): void { - localStorage.removeItem(STORAGE_KEY); -} - -/** Attaches the stored API key to every request; the caller handles a 401 by clearing it and re-prompting. */ -export async function apiFetch(path: string, init: RequestInit = {}): Promise { - const apiKey = getStoredApiKey(); - const headers = new Headers(init.headers); - if (apiKey) headers.set('x-api-key', apiKey); - if (init.body && !headers.has('Content-Type')) headers.set('Content-Type', 'application/json'); - return fetch(path, { ...init, headers }); -} - -export function openStatusSocket(): WebSocket { - const apiKey = getStoredApiKey() ?? ''; - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - return new WebSocket(`${protocol}//${window.location.host}/ws?apiKey=${encodeURIComponent(apiKey)}`); -} diff --git a/src/bridge/lottie.ts b/src/bridge/lottie.ts index d204c52..7e74aff 100644 --- a/src/bridge/lottie.ts +++ b/src/bridge/lottie.ts @@ -16,7 +16,7 @@ import { gunzipSync } from 'node:zlib'; import { spawn } from 'node:child_process'; import { mkdtemp, readFile, rm } from 'node:fs/promises'; -import { readFileSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; import puppeteer from 'puppeteer-core'; @@ -25,6 +25,15 @@ import { createLogger } from '../logger.js'; const logger = createLogger('lottie'); const CHROMIUM_PATH = process.env.CHROMIUM_PATH || '/usr/bin/chromium'; + +/** + * Whether this image can render animated (.tgs) stickers to video. The "slim" build (STICKERS=slim, + * see Dockerfile) omits Chromium+ffmpeg to save ~1.4 GB; there, animated stickers relay as their + * static thumbnail instead. Presence of the Chromium binary IS the signal — no separate flag needed. + */ +export function canRenderAnimatedStickers(): boolean { + return existsSync(CHROMIUM_PATH); +} // esbuild bundles this whole module into dist/server.mjs — import.meta.url at // runtime points at THAT file's location (/app/dist/server.mjs), not this // source file's, so the relative path has to be resolved from there. diff --git a/src/bridge/panel.ts b/src/bridge/panel.ts index 850d5b3..09e4bc2 100644 --- a/src/bridge/panel.ts +++ b/src/bridge/panel.ts @@ -19,8 +19,6 @@ export interface ControlPanelDeps { /** Reused slash-command bodies — the panel just triggers the same output. */ leaves: { sendHelp: LeafSender; - sendLoginLink: LeafSender; - sendApiKey: LeafSender; sendVersion: LeafSender; sendBanList: LeafSender; sendUnbanList: LeafSender; @@ -89,7 +87,7 @@ function rootView(phone: string): View { text: `🎛 Telemax — пульт\n${status} · версия ${version}`, markup: Markup.inlineKeyboard([ [Markup.button.callback('👤 Найти контакт', 'tlmx_panel:contacts'), Markup.button.callback('🚫 Чаты', 'tlmx_panel:chats')], - [Markup.button.callback('🌐 Веб-панель', 'tlmx_panel:web'), Markup.button.callback('⚙️ Система', 'tlmx_panel:system')], + [Markup.button.callback('🔐 Вход в MAX', 'tlmx_panel:web'), Markup.button.callback('⚙️ Система', 'tlmx_panel:system')], ]).reply_markup, }; } @@ -112,15 +110,16 @@ function chatsView(): View { }; } function webView(botUsername?: string): View { - // The MAX-login button is a deep link into the bot's DM (t.me/?start=login) — auth (SMS code, - // 2FA password) happens privately there, not in the group. Shown only once we know the username. + // Auth is a deep link into the bot's DM (t.me/?start=login) — SMS code and 2FA password + // are entered privately there, never in the group. Button shown only once we know the username. const rows = [ - [Markup.button.callback('🔗 Ссылка для входа', 'tlmx_panel:link'), Markup.button.callback('🔑 API-ключ', 'tlmx_panel:apikey')], ...(botUsername ? [[Markup.button.url('🔐 Войти в MAX', `https://t.me/${botUsername}?start=login`)]] : []), [Markup.button.callback('◀️ Назад', 'tlmx_panel:root')], ]; return { - text: '🌐 Веб-панель:', + text: botUsername + ? '🔐 Вход в MAX — в личке бота:\nнажмите кнопку (или напишите боту в личку /login). Код и пароль не попадут в группу.' + : '🔐 Вход в MAX: напишите боту в личку /login.', markup: Markup.inlineKeyboard(rows).reply_markup, }; } @@ -233,14 +232,6 @@ export function wireControlPanel(deps: ControlPanelDeps): void { await ctx.answerCbQuery(); await leaves.sendHelp(chatIdOf(ctx)); }); - bot.action('tlmx_panel:apikey', async (ctx) => { - await ctx.answerCbQuery(); - await leaves.sendApiKey(chatIdOf(ctx)); - }); - bot.action('tlmx_panel:link', async (ctx) => { - await ctx.answerCbQuery(); - await leaves.sendLoginLink(chatIdOf(ctx)); - }); bot.action('tlmx_panel:update', async (ctx) => { await ctx.answerCbQuery(); await leaves.sendVersion(chatIdOf(ctx)); diff --git a/src/bridge/sync.ts b/src/bridge/sync.ts index 325ff94..8bcdb95 100644 --- a/src/bridge/sync.ts +++ b/src/bridge/sync.ts @@ -3,7 +3,6 @@ import { writeFile } from 'node:fs/promises'; import { existsSync } from 'node:fs'; import { Markup, type Telegraf } from 'telegraf'; import type { ChatAction, TelegramEmoji } from 'telegraf/types'; -import { Agent, fetch as undiciFetch } from 'undici'; import type { MaxClient, MaxMessageEvent, MaxHistoryMessage } from '../max/client.js'; import { OPCODES, formatOpcode } from '../max/opcodes.js'; import { resolveContactDisplayName, resolveChatName, type ContactProfile } from '../max/names.js'; @@ -11,6 +10,7 @@ import type { ChatMapStore } from '../store/chatMapStore.js'; import { ensureTopicForMaxChat } from '../telegram/bot.js'; import { downloadMaxAttachment, describeAttachment, type MaxAttachment, type DownloadContext } from './attachments.js'; import { uploadTelegramAttachmentToMax } from './upload.js'; +import { canRenderAnimatedStickers } from './lottie.js'; import { reportBridgeError } from './errorReporter.js'; import { wireControlPanel } from './panel.js'; import { createBugReports, BUGREPORT_BOT_HANDLE, type BugReports } from './bugReports.js'; @@ -668,32 +668,6 @@ async function pinInfoCard(bot: Telegraf, targetGroupId: string, messageId: numb } } -// ifconfig.me is dual-stack; Node's built-in fetch (undici) doesn't reliably honor the process -// ipv4-first DNS order, so it can answer over IPv6 and echo back the server's IPv6 address. That IPv6 -// then lands in the /apikey panel link — unbracketed (a broken URL) and pointing at a family the panel -// doesn't even serve (it listens on 0.0.0.0, IPv4 only). Pin this one probe to IPv4 so the login link -// is always a reachable IPv4 URL. Reported live: a dual-stack user's panel button handed out IPv6. -const ipv4OnlyDispatcher = new Agent({ connect: { family: 4 } }); - -/** Same lookup setup.sh does once at install time, run live for /apikey's link — best-effort, `null` just falls back to the bare key. */ -async function detectPublicIp(): Promise { - try { - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 5000); - const res = await undiciFetch('https://ifconfig.me/ip', { - signal: controller.signal, - dispatcher: ipv4OnlyDispatcher, - }).finally(() => clearTimeout(timeout)); - if (!res.ok) return null; - const ip = (await res.text()).trim(); - // Safety net: should be IPv4 given the dispatcher above, but never emit a bare IPv6 into a URL. - return ip.includes(':') ? `[${ip}]` : ip; - } catch (err) { - logger.error('Failed to detect public IP for /apikey link', err); - return null; - } -} - /** Picked up within a minute by update-watcher.sh on the host (see setup.sh) — writing it is the only thing the container itself does towards an update, everything else (git pull, rebuild, restart) happens outside it. */ const UPDATE_REQUESTED_MARKER = path.join(process.cwd(), '.data', 'update-requested'); /** Written by update.sh while it runs (data/ is the same mount as .data/) — lets the /version button refuse a second request mid-update. */ @@ -732,8 +706,6 @@ export interface BridgeOptions { getMyAccountId: () => number | null; getContactProfiles: () => Map; getActivePhone: () => string; - /** 'https' once the panel booted with its (self-signed) certificate, 'http' when TLS is off or cert setup failed — /apikey builds its login link with it. */ - getPanelScheme: () => 'http' | 'https'; /** Refetches MAX's chat list and re-runs the full backfill sync — used by /reboot after wiping local state. Fire-and-forget on the caller's side (server/app.ts already guards against overlapping runs). */ triggerFullResync: () => Promise; /** Disconnects from MAX and deletes the encrypted session — used by /kill. Awaited (unlike triggerFullResync) since /kill's own confirmation message should only go out once this has actually finished. */ @@ -756,7 +728,6 @@ export function wireBridge({ getMyAccountId, getContactProfiles, getActivePhone, - getPanelScheme, triggerFullResync, killEverything, auth, @@ -1584,29 +1555,6 @@ export function wireBridge({ }); /** Recovers the web-panel API_KEY without needing SSH/file access to the server — safe now that the target-group middleware above actually gates who can ask. Bundles it into a ready-to-open link (App.tsx reads ?key= and logs straight in) when the server's public IP can be detected, falling back to the bare key otherwise. */ - bot.command('apikey', async (ctx) => { - const apiKey = process.env.API_KEY; - if (!apiKey) { - await bot.telegram.sendMessage(ctx.chat.id, 'API_KEY не задан в .env.', { message_thread_id: ctx.message.message_thread_id }); - return; - } - const port = process.env.PORT ?? '3000'; - const ip = await detectPublicIp(); - const scheme = getPanelScheme(); - const certNote = - scheme === 'https' - ? '\n\n🔒 Сертификат самоподписанный — браузер один раз предупредит «подключение не защищено»: жми «Дополнительно» → «Перейти на сайт».' - : ''; - const text = ip - ? `🔑 Вход в веб-панель (ссылка сразу авторизует):\n${scheme}://${ip}:${port}/?key=${encodeURIComponent(apiKey)}${certNote}` - : `🔑 Ключ для веб-панели (не удалось определить IP сервера — откройте панель вручную и введите ключ):\n${apiKey}${certNote}`; - await bot.telegram.sendMessage(ctx.chat.id, text, { - message_thread_id: ctx.message.message_thread_id, - parse_mode: 'HTML', - link_preview_options: { is_disabled: true }, - }); - }); - // In-Telegram MAX login. In a DM the private-chat auth flow already handles /login; this group // handler just hands over the deep link into that DM, so the SMS code and 2FA password never // touch the group chat. @@ -1795,31 +1743,6 @@ export function wireBridge({ const { text, replyMarkup } = formatVersionMessage(await checkVersion()); await bot.telegram.sendMessage(chatId, text, { reply_markup: replyMarkup }); }, - sendApiKey: async (chatId) => { - const key = process.env.API_KEY; - await bot.telegram.sendMessage(chatId, key ? `🔑 Ключ веб-панели:\n${key}` : 'API_KEY не задан в .env.', { - parse_mode: 'HTML', - link_preview_options: { is_disabled: true }, - }); - }, - sendLoginLink: async (chatId) => { - const apiKey = process.env.API_KEY; - if (!apiKey) { - await bot.telegram.sendMessage(chatId, 'API_KEY не задан в .env.'); - return; - } - const port = process.env.PORT ?? '3000'; - const ip = await detectPublicIp(); - const scheme = getPanelScheme(); - const certNote = - scheme === 'https' - ? '\n\n🔒 Сертификат самоподписанный — браузер предупредит один раз: «Дополнительно» → «Перейти на сайт».' - : ''; - const text = ip - ? `🔗 Вход в веб-панель (ссылка сразу авторизует):\n${scheme}://${ip}:${port}/?key=${encodeURIComponent(apiKey)}${certNote}` - : `🔗 Не удалось определить IP сервера — откройте панель вручную и введите ключ (кнопка «API-ключ»).${certNote}`; - await bot.telegram.sendMessage(chatId, text, { parse_mode: 'HTML', link_preview_options: { is_disabled: true } }); - }, sendBanList: async (chatId) => { const active = (await chatMapStore.list()).filter((m) => !m.banned); if (active.length === 0) { @@ -2259,7 +2182,7 @@ export function wireBridge({ const video = (ctx.message as { video?: { file_id: string; file_name?: string } }).video; const videoNote = (ctx.message as { video_note?: { file_id: string } }).video_note; const voice = (ctx.message as { voice?: { file_id: string; duration?: number } }).voice; - const sticker = (ctx.message as { sticker?: { file_id: string; is_animated?: boolean; is_video?: boolean } }).sticker; + const sticker = (ctx.message as { sticker?: { file_id: string; is_animated?: boolean; is_video?: boolean; thumbnail?: { file_id: string } } }).sticker; const poll = (ctx.message as { poll?: { id: string; question: string; options: Array<{ text: string }>; is_anonymous: boolean; allows_multiple_answers: boolean } }).poll; const location = (ctx.message as { location?: { latitude: number; longitude: number } }).location; const contact = (ctx.message as { contact?: { phone_number: string; first_name: string; last_name?: string } }).contact; @@ -2395,7 +2318,13 @@ export function wireBridge({ if (sticker.is_video) { attach = await uploadTelegramAttachmentToMax(bot, max, sticker.file_id, 'video', 'sticker.webm'); } else if (sticker.is_animated) { - attach = await uploadTelegramAttachmentToMax(bot, max, sticker.file_id, 'sticker_animated'); + // Animated .tgs needs a headless-Chromium render (see lottie.ts). On a "slim" image without + // Chromium, fall back to the sticker's static thumbnail so something still comes through. + if (canRenderAnimatedStickers()) { + attach = await uploadTelegramAttachmentToMax(bot, max, sticker.file_id, 'sticker_animated'); + } else if (sticker.thumbnail?.file_id) { + attach = await uploadTelegramAttachmentToMax(bot, max, sticker.thumbnail.file_id, 'photo'); + } } else { attach = await uploadTelegramAttachmentToMax(bot, max, sticker.file_id, 'photo'); } diff --git a/src/index.css b/src/index.css deleted file mode 100644 index f1d8c73..0000000 --- a/src/index.css +++ /dev/null @@ -1 +0,0 @@ -@import "tailwindcss"; diff --git a/src/main.tsx b/src/main.tsx deleted file mode 100644 index dfacde0..0000000 --- a/src/main.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import App from './App'; -import './index.css'; - -createRoot(document.getElementById('root')!).render( - - - , -); diff --git a/src/server/app.ts b/src/server/app.ts index 31a4378..934d5f0 100644 --- a/src/server/app.ts +++ b/src/server/app.ts @@ -1,37 +1,20 @@ -import express from 'express'; import dns from 'node:dns'; -import { createServer } from 'node:http'; -import { createServer as createTlsServer } from 'node:https'; -import net from 'node:net'; -import { execFile } from 'node:child_process'; -import { promisify } from 'node:util'; -import { WebSocketServer, type WebSocket } from 'ws'; import { Telegraf } from 'telegraf'; import path from 'node:path'; -import { chmod, mkdir, readFile, unlink, writeFile } from 'node:fs/promises'; +import { readFile, unlink, writeFile } from 'node:fs/promises'; import { MaxClient, type MaxMessageEvent, type MaxContactInfo } from '../max/client.js'; -import { OPCODES, formatOpcode } from '../max/opcodes.js'; +import { OPCODES } from '../max/opcodes.js'; import { extractMyAccountId, resolveChatName, type ContactProfile } from '../max/names.js'; import { SessionStore, type MaxSession } from '../store/sessionStore.js'; import { ChatMapStore } from '../store/chatMapStore.js'; import { wireBridge, syncAllChatsToTelegram, MessageLinkStore } from '../bridge/sync.js'; import { configureErrorReporter, reportBridgeError, resetErrorKey } from '../bridge/errorReporter.js'; import { getAppVersion } from '../bridge/version.js'; -import { createLogger, jsonStringify, redactSecrets } from '../logger.js'; +import { createLogger } from '../logger.js'; import { config } from './config.js'; -import { isValidApiKey, requireApiKey } from './authMiddleware.js'; -import { - buildTelegramProxyAgent, - getResolvedProxyUrl, - getTelegramProxyAgent, - initTelegramProxy, - redactProxyUrl, - writePersistedProxy, -} from '../telegram/proxy.js'; -import https from 'node:https'; +import { getTelegramProxyAgent, initTelegramProxy } from '../telegram/proxy.js'; const logger = createLogger('server'); -const startedAt = Date.now(); // Prefer IPv4 when a host resolves to both. On dual-stack servers where IPv6 has no // working route to Telegram (common in RU — Telegram is blocked over v6 while v4 @@ -74,9 +57,6 @@ let pendingAuthToken: string | null = null; /** Set when verifyCode() comes back password_required — cleared only on a successful login, since the trackId survives a wrong password and can be retried (confirmed live 2026-08-14). */ let pendingPasswordTrackId: string | null = null; let currentSession: MaxSession | null = null; -let latestLatencyMs: number | null = null; -let packetsSent = 0; -let packetsReceived = 0; // LOGIN's embedded chat list is capped at chatsCount (<=50) and, live, has also // been observed to just omit chats a later resumed-session LOGIN included — // cachedChats gets replaced with the real, fully-paginated CHATS_LIST result @@ -148,7 +128,6 @@ async function completeMaxLogin(loginToken: string): Promise { activePhone = session.phone; lastKnownPhone = session.phone; notifyMaxSessionRestored(); - broadcastStatus(); refreshBotDescription(); } @@ -237,7 +216,6 @@ async function killMaxSession(): Promise { myAccountId = null; contactProfiles = new Map(); maxConnected = false; - broadcastStatus(); refreshBotDescription(); } @@ -261,88 +239,6 @@ function upsertCachedChat(chat: unknown): void { else cachedChats.push(chat); } -interface UiLogEntry { - time: string; - opcode: string; - dir: string; - size: string; - payload: string; -} - -const packetLogs: UiLogEntry[] = []; -const uiClients = new Set(); - -function broadcast(message: unknown): void { - const data = JSON.stringify(message); - for (const client of uiClients) { - if (client.readyState === client.OPEN) client.send(data); - } -} - -function broadcastStatus(): void { - broadcast({ - type: 'status', - data: { - max: maxConnected, - tg: tgActive, - deviceId: max.deviceId, - phone: activePhone, - latencyMs: latestLatencyMs, - }, - }); -} - -function pushLog(dir: string, opcode: number, length: number, payload: unknown): void { - const entry: UiLogEntry = { - time: new Date().toISOString().substring(11, 23), - opcode: formatOpcode(opcode), - dir, - size: `${length} B`, - payload: jsonStringify(redactSecrets(payload)), - }; - packetLogs.push(entry); - if (packetLogs.length > 50) packetLogs.shift(); - broadcast({ type: 'log', data: entry }); -} - -const execFileAsync = promisify(execFile); -const TLS_DIR = path.join(process.cwd(), '.data', 'tls'); - -/** What /apikey should build its login link with — flips to 'https' once the panel actually boots with a certificate (not just intends to), so the link never points at a scheme the server isn't serving. */ -let panelScheme: 'http' | 'https' = 'http'; - -/** - * Self-signed TLS for the web panel. Installs are typically bare-IP VPSes, so a - * publicly-trusted certificate isn't attainable by default — self-signed still - * closes the real gap: the API key, the SMS code and the MAX 2FA password used - * to cross the open internet as plain HTTP. The browser warns once about the - * unknown issuer (expected; /apikey's link says so). Generated with the openssl - * CLI (present in the Docker image) and persisted in .data/tls — the ./data - * volume — so the browser exception survives container rebuilds. - */ -async function loadOrCreatePanelCert(): Promise<{ key: Buffer; cert: Buffer } | null> { - const keyPath = path.join(TLS_DIR, 'key.pem'); - const certPath = path.join(TLS_DIR, 'cert.pem'); - try { - return { key: await readFile(keyPath), cert: await readFile(certPath) }; - } catch { - // not generated yet — fall through - } - try { - await mkdir(TLS_DIR, { recursive: true }); - await execFileAsync('openssl', [ - 'req', '-x509', '-newkey', 'rsa:2048', '-keyout', keyPath, '-out', certPath, - '-days', '3650', '-nodes', '-subj', '/CN=telemax', - ]); - await chmod(keyPath, 0o600).catch(() => {}); - logger.info('Generated a self-signed TLS certificate for the web panel (.data/tls)'); - return { key: await readFile(keyPath), cert: await readFile(certPath) }; - } catch (err) { - logger.error('Failed to generate a self-signed TLS certificate — the panel stays on plain HTTP', err); - return null; - } -} - // Set once we've told the group the MAX session was lost, so a "restored" notice only // fires after an actual loss — and only once. let maxSessionLostReported = false; @@ -446,7 +342,6 @@ async function loginWithSession(session: MaxSession): Promise { } finally { resumeInFlight = false; } - broadcastStatus(); refreshBotDescription(); } @@ -463,7 +358,6 @@ async function startServer(): Promise { max.on('connected', () => { maxConnected = true; - broadcastStatus(); if (maxDownTimer) { clearTimeout(maxDownTimer); maxDownTimer = null; @@ -481,7 +375,6 @@ async function startServer(): Promise { max.on('disconnected', () => { maxConnected = false; - broadcastStatus(); if (maxDownTimer == null && !maxDownReported) { maxDownTimer = setTimeout(() => { maxDownTimer = null; @@ -493,21 +386,7 @@ async function startServer(): Promise { max.on('error', (err: Error) => logger.error('MAX client error:', err.message)); - max.on('latency', (ms: number) => { - latestLatencyMs = ms; - broadcastStatus(); - }); - - max.on('sent', ({ opcode, payload, length }: { opcode: number; payload: unknown; length: number }) => { - packetsSent += 1; - pushLog('TX', opcode, length, payload); - }); - max.on('message', (event: MaxMessageEvent) => { - packetsReceived += 1; - const dirLabel = event.dir === 0x03 ? 'ERR' : event.dir === 0x01 ? 'RX' : 'PUSH'; - pushLog(dirLabel, event.opcode, event.length, event.payload); - if (event.opcode === OPCODES.PUSH_MESSAGE || (event.opcode === OPCODES.MSG_SEND && event.dir === 0x01)) { const p = event.payload as { chatId?: number; message?: unknown } | null; if (p?.chatId != null && p.message) patchCachedChatLastMessage(p.chatId, p.message); @@ -541,7 +420,6 @@ async function startServer(): Promise { getMyAccountId: () => myAccountId, getContactProfiles: () => contactProfiles, getActivePhone: () => activePhone, - getPanelScheme: () => panelScheme, triggerFullResync: () => refreshChatsAndNames().then(() => syncChatsIfPossible()), killEverything: killMaxSession, auth: { @@ -562,260 +440,14 @@ async function startServer(): Promise { logger.warn('TELEGRAM_BOT_TOKEN / TARGET_TELEGRAM_GROUP not set — Telegram bridge stays disabled'); } - // --- HTTP + WS server --- - const app = express(); - app.use(express.json()); + // No inbound server: the bridge is headless (v0.4). Auth, status and control all live in + // the Telegram bot now (see /login, the /panel menu). MAX runs over an outbound TCP socket + // and Telegram over long-polling, so nothing needs to listen on a port. - // TLS on by default in production (self-signed — see loadOrCreatePanelCert). - // PANEL_TLS=off opts out for setups with their own TLS-terminating reverse - // proxy in front. Dev mode stays plain HTTP on localhost. - const wantTls = process.env.NODE_ENV === 'production' && process.env.PANEL_TLS !== 'off'; - const tlsMaterial = wantTls ? await loadOrCreatePanelCert() : null; - panelScheme = tlsMaterial ? 'https' : 'http'; - const httpServer = tlsMaterial ? createTlsServer({ key: tlsMaterial.key, cert: tlsMaterial.cert }, app) : createServer(app); - const wss = new WebSocketServer({ noServer: true }); - - httpServer.on('upgrade', (req, socket, head) => { - if (req.url?.startsWith('/ws')) { - const key = new URL(req.url, 'http://localhost').searchParams.get('apiKey'); - if (!isValidApiKey(config.apiKey, key)) { - socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); - socket.destroy(); - return; - } - wss.handleUpgrade(req, socket, head, (ws) => wss.emit('connection', ws, req)); - } else { - socket.destroy(); - } - }); - - wss.on('connection', (ws) => { - uiClients.add(ws); - ws.send(JSON.stringify({ type: 'init_logs', data: packetLogs })); - ws.send( - JSON.stringify({ - type: 'status', - data: { max: maxConnected, tg: tgActive, deviceId: max.deviceId, phone: activePhone, latencyMs: latestLatencyMs }, - }), - ); - ws.on('close', () => uiClients.delete(ws)); - }); - - app.get('/api/health', (_req, res) => res.json({ ok: true })); - - const api = express.Router(); - api.use(requireApiKey(config.apiKey)); - - api.get('/status', (_req, res) => { - res.json({ - maxOnline: maxConnected, - tgActive, - deviceId: max.deviceId, - phone: activePhone, - latencyMs: latestLatencyMs, - uptimeSeconds: Math.floor((Date.now() - startedAt) / 1000), - packetsSent, - packetsReceived, - }); - }); - - api.post('/system/stop', (_req, res) => { - max.disconnect(); - res.json({ success: true }); - }); - - api.post('/system/start', (_req, res) => { - max.connect(); - res.json({ success: true }); - }); - - // Restarts the whole process to apply config that's only read at startup (the - // Telegram proxy). compose's `restart: unless-stopped` brings the container right - // back, so from the operator's side this is just "apply & reconnect". - api.post('/system/restart', (_req, res) => { - res.json({ success: true }); - logger.warn('Restart requested from the web panel — exiting so the container comes back up'); - setTimeout(() => process.exit(0), 300); - }); - - // --- Telegram proxy config (see src/telegram/proxy.ts) --- - api.get('/proxy', (_req, res) => { - res.json({ proxy: getResolvedProxyUrl() }); - }); - - api.post('/proxy/test', async (req, res) => { - const url = typeof req.body?.proxy === 'string' ? req.body.proxy : ''; - res.json(await testTelegramProxyReachable(url)); - }); - - api.post('/proxy', async (req, res) => { - const url = typeof req.body?.proxy === 'string' ? req.body.proxy.trim() : ''; - try { - buildTelegramProxyAgent(url); // validates scheme/format — throws on garbage - } catch (err) { - res.status(400).json({ error: (err as Error).message }); - return; - } - await writePersistedProxy(url); - logger.info(url ? `Telegram proxy saved (${redactProxyUrl(url)}) — applies on restart` : 'Telegram proxy cleared — applies on restart'); - res.json({ success: true, restartRequired: true }); - }); - - api.post('/auth/phone', async (req, res) => { - const rawPhone = req.body?.phone; - if (!rawPhone) { - res.status(400).json({ error: 'phone missing' }); - return; - } - try { - await maxAuthRequestSms(String(rawPhone)); - res.json({ success: true }); - } catch (err) { - const msg = (err as Error).message; - logger.error('Failed to request SMS', err); - res.status(msg === 'MAX is disconnected' ? 503 : 502).json({ error: msg }); - } - }); - - api.post('/auth/verify', async (req, res) => { - const code = req.body?.code; - if (!code) { - res.status(400).json({ error: 'code missing' }); - return; - } - if (!pendingAuthToken) { - res.status(400).json({ error: 'no pending auth — call /api/auth/phone first' }); - return; - } - try { - const result = await maxAuthVerifyCode(String(code)); - if ('passwordRequired' in result) { - res.json({ success: true, passwordRequired: true, hint: result.hint }); - return; - } - res.json({ success: true }); - } catch (err) { - logger.error('Failed to verify SMS code', err); - res.status(502).json({ error: (err as Error).message }); - } - }); - - // Only reached for password-protected accounts — verifyCode() above set - // pendingPasswordTrackId instead of completing the login directly. - api.post('/auth/password', async (req, res) => { - const password = req.body?.password; - if (!password) { - res.status(400).json({ error: 'password missing' }); - return; - } - if (!pendingPasswordTrackId) { - res.status(400).json({ error: 'no pending password challenge — call /api/auth/verify first' }); - return; - } - try { - await maxAuthCheckPassword(String(password)); - res.json({ success: true }); - } catch (err) { - logger.error('Failed to verify MAX password', err); - res.status(502).json({ error: (err as Error).message }); - } - }); - - // Same MAX-side teardown /kill's bot command uses (disconnect + delete the - // encrypted session) — but scoped to just that, unlike /kill, which also - // wipes every Telegram topic. Lets the web panel offer "log out / change - // number" without touching chat history. - api.post('/auth/logout', async (_req, res) => { - await killMaxSession(); - res.json({ success: true }); - }); - - api.get('/chats', (_req, res) => { - const chats = cachedChats.map((chat) => ({ - ...(chat as object), - displayName: resolveChatName(chat, myAccountId, contactProfiles), - })); - res.type('application/json').send(jsonStringify({ success: true, data: { chats } })); - }); - - api.get('/chat-mappings', async (_req, res) => { - res.json({ success: true, data: await chatMapStore.list() }); - }); - - // The /api/debug/* one-shot probes that used to live here (test-poll, test-contact, - // test-group*, test-delete, chats-list, contact-info, chat-history) were removed - // 2026-08-14 after every probed opcode got wired into real bot commands — they were - // API-key-gated but still let a caller send messages and manage groups on the MAX - // account, which is needless surface on a production install. Recover from git - // history if a new opcode ever needs live probing again. - - api.post('/chats/:id/messages', async (req, res) => { - if (!maxConnected) { - res.status(503).json({ error: 'MAX is disconnected' }); - return; - } - const text = req.body?.text; - if (!text) { - res.status(400).json({ error: 'text missing' }); - return; - } - try { - const { cid } = await max.sendMessage(req.params.id, String(text)); - res.json({ success: true, cid }); - } catch (err) { - res.status(502).json({ error: (err as Error).message }); - } - }); - - app.use('/api', api); - - // --- Frontend --- - if (process.env.NODE_ENV === 'production') { - const distPath = path.join(process.cwd(), 'dist'); - app.use(express.static(distPath)); - app.get('*', (_req, res) => res.sendFile(path.join(distPath, 'index.html'))); - } else { - // Dynamic import, NOT a static top-level one: vite is a devDependency, so it - // doesn't exist in the production image at all. A static import crashed the - // container on boot (ERR_MODULE_NOT_FOUND) the moment @tailwindcss/vite left - // "dependencies" — it had been pulling vite into the runtime image as its - // peer dependency this whole time, masking the problem. Hit live 2026-08-14. - const { createServer: createViteServer } = await import('vite'); - const vite = await createViteServer({ server: { middlewareMode: true }, appType: 'spa' }); - app.use(vite.middlewares); - } - - let listener: net.Server = httpServer; - if (tlsMaterial) { - // Single-port polyglot: a TLS ClientHello always starts with byte 0x16, so - // anything else on the socket is a plain-HTTP client — an old bookmark or a - // pre-TLS /apikey link — and gets a 301 to the same URL over https instead - // of a cryptic protocol error. - const redirectServer = createServer((req, res) => { - res.writeHead(301, { Location: `https://${req.headers.host ?? `localhost:${config.port}`}${req.url ?? '/'}` }); - res.end(); - }); - listener = net.createServer((socket) => { - socket.on('error', () => socket.destroy()); - socket.once('data', (firstChunk) => { - socket.pause(); - socket.unshift(firstChunk); - (firstChunk[0] === 0x16 ? httpServer : redirectServer).emit('connection', socket); - // NOT a synchronous resume: the TLS wrap set up by the 'connection' - // listener needs this tick, or the handshake never sees the ClientHello - // and hangs forever (caught by a local smoke test before shipping). - process.nextTick(() => socket.resume()); - }); - }); - } - listener.listen(config.port, '0.0.0.0', () => { - logger.info(`Server listening on port ${config.port}${tlsMaterial ? ' (HTTPS, self-signed)' : ''}`); - }); - - // docker stop / systemd send SIGTERM (node runs as PID 1 — exec-form CMD, so it - // actually receives it). Stop polling Telegram and close the MAX socket cleanly - // instead of letting the runtime kill mid-write; the backfill cursor is persisted - // per message, so an in-flight backfill resumes where it left off either way. + // docker stop / systemd send SIGTERM (node runs as PID 1 — exec-form CMD, so it actually + // receives it). Stop polling Telegram and close the MAX socket cleanly instead of letting the + // runtime kill mid-write; the backfill cursor is persisted per message, so an in-flight + // backfill resumes where it left off either way. const shutdown = (signal: string): void => { logger.info(`Received ${signal}, shutting down`); try { @@ -824,11 +456,9 @@ async function startServer(): Promise { // bot may not have launched (bad token, mid-retry) — nothing to stop } max.disconnect(); - if (listener !== httpServer) httpServer.close(); - listener.close(() => process.exit(0)); - // Failsafe: don't let a lingering keep-alive socket hold the process past - // docker's own stop timeout. + // Failsafe: don't let a lingering keep-alive socket hold the process past docker's stop timeout. setTimeout(() => process.exit(0), 5_000).unref(); + process.exit(0); }; process.once('SIGTERM', () => shutdown('SIGTERM')); process.once('SIGINT', () => shutdown('SIGINT')); @@ -869,7 +499,6 @@ const BOT_COMMANDS = [ { command: 'panel', description: '🎛 Пульт управления (меню с кнопками)' }, { command: 'help', description: 'Полный список команд и ограничений' }, { command: 'donate', description: 'Поддержать проект (рубли / TON)' }, - { command: 'apikey', description: 'Показать ключ для веб-панели' }, { command: 'login', description: 'Войти в MAX (номер + SMS, в личке бота)' }, { command: 'version', description: 'Версия бота, обновление по кнопке' }, { command: 'info', description: 'Карточка контакта/чата (просто в теме)' }, @@ -945,7 +574,6 @@ async function announceGroupReadyOnce(bot: Telegraf): Promise { function retryTelegramLaunch(bot: Telegraf, attempt: number, reason: unknown): void { tgActive = false; - broadcastStatus(); const idx = Math.min(attempt, TELEGRAM_RETRY_DELAYS_MS.length - 1); const delay = TELEGRAM_RETRY_DELAYS_MS[idx] as number; logger.error(`Telegram bot stopped, retrying in ${delay}ms:`, reason instanceof Error ? reason.message : reason); @@ -970,7 +598,6 @@ async function launchTelegramBotWithRetry(bot: Telegraf, attempt = 0): Promise retryTelegramLaunch(bot, attempt, err)); } -/** Tries to reach api.telegram.org through the given proxy URL (empty = direct). Backs - * the panel's "test" button before a proxy is saved. Never throws — returns a verdict. */ -async function testTelegramProxyReachable(proxyUrl: string): Promise<{ ok: boolean; error?: string }> { - let agent: ReturnType; - try { - agent = buildTelegramProxyAgent(proxyUrl); - } catch (err) { - return { ok: false, error: (err as Error).message }; - } - return new Promise((resolve) => { - const req = https.get('https://api.telegram.org/', { agent, timeout: 8000 }, (res) => { - res.resume(); - resolve({ ok: true }); - }); - req.on('timeout', () => { - req.destroy(); - resolve({ ok: false, error: 'таймаут подключения' }); - }); - req.on('error', (err) => resolve({ ok: false, error: err.message })); - }); -} - function createBotSafely(token: string): Telegraf | null { try { const agent = getTelegramProxyAgent(); diff --git a/src/server/authMiddleware.ts b/src/server/authMiddleware.ts deleted file mode 100644 index 535e6b9..0000000 --- a/src/server/authMiddleware.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { timingSafeEqual } from 'node:crypto'; -import type { RequestHandler } from 'express'; - -export function isValidApiKey(expected: string, provided: string | null | undefined): boolean { - if (!provided) return false; - const expectedBuf = Buffer.from(expected); - const providedBuf = Buffer.from(provided); - return providedBuf.length === expectedBuf.length && timingSafeEqual(providedBuf, expectedBuf); -} - -/** Requires a matching `x-api-key` header. Applied to every /api/* route except /api/health (ТЗ.md §3.4). */ -export function requireApiKey(expected: string): RequestHandler { - return (req, res, next) => { - if (!isValidApiKey(expected, req.header('x-api-key'))) { - res.status(401).json({ error: 'missing or invalid x-api-key' }); - return; - } - next(); - }; -} diff --git a/src/server/config.ts b/src/server/config.ts index 36d1e58..4df25f3 100644 --- a/src/server/config.ts +++ b/src/server/config.ts @@ -1,17 +1,6 @@ import 'dotenv/config'; -function required(name: string): string { - const value = process.env[name]; - if (!value) { - throw new Error(`${name} is not set — see .env.example`); - } - return value; -} - export const config = { - port: Number(process.env.PORT ?? 3000), - // Shared secret required on every /api/* route except /api/health (ТЗ.md §3.4). - apiKey: required('API_KEY'), telegramBotToken: process.env.TELEGRAM_BOT_TOKEN ?? '', targetTelegramGroup: process.env.TARGET_TELEGRAM_GROUP ?? '', // Override the MAX endpoint. Unset → connect by hostname (api2.oneme.ru), which lets diff --git a/tsconfig.json b/tsconfig.json index ea0ce8b..3c2c7c7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,5 @@ "rootDir": ".", "types": ["node"] }, - "include": ["src", "scripts", "tests"], - "exclude": ["src/App.tsx", "src/main.tsx", "src/api.ts"] + "include": ["src", "scripts", "tests"] } diff --git a/tsconfig.web.json b/tsconfig.web.json deleted file mode 100644 index c1cc5da..0000000 --- a/tsconfig.web.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "Bundler", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "jsx": "react-jsx", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "resolveJsonModule": true, - "noEmit": true, - "types": ["vite/client"] - }, - "include": ["src/App.tsx", "src/main.tsx", "src/api.ts"] -} diff --git a/update.sh b/update.sh index 36c04ac..0309dcf 100755 --- a/update.sh +++ b/update.sh @@ -64,6 +64,36 @@ fi # two up originally, which silently broke the whole watcher — confirmed live # 2026-08-14: a marker written straight to `data/` was never picked up # because update-watcher.sh was checking `.data/` instead). +STEP="проверка подписи релиза" +# Signed-release trust (v0.4): the transport (GitHub OR the self-hosted mirror) is untrusted — only +# the maintainer's GPG signature on the release tag is. When release-signing-key.asc is pinned in the +# repo, REQUIRE a valid signed tag at the pulled HEAD before building anything, so a compromised +# GitHub/mirror can't auto-deploy code. Absent (pre-signing installs, mid-transition) → skip. +PUBKEY_FILE="release-signing-key.asc" +if [ -f "$PUBKEY_FILE" ]; then + if ! command -v gpg >/dev/null 2>&1; then + notify "❌ Обновление отклонено: не установлен gpg для проверки подписи релиза (apt-get install -y gnupg)." + exit 1 + fi + VERIFY_HOME=$(mktemp -d) + GNUPGHOME="$VERIFY_HOME" gpg --quiet --import "$PUBKEY_FILE" >/dev/null 2>&1 || true + TAG_AT_HEAD=$(git tag --points-at HEAD 2>/dev/null | grep -E '^v[0-9]' | sort -V | tail -1 || true) + if [ -z "$TAG_AT_HEAD" ]; then + rm -rf "$VERIFY_HOME" + notify "❌ Обновление отклонено: на новой версии нет подписанного тега." + exit 1 + fi + if ! GNUPGHOME="$VERIFY_HOME" git -c gpg.program=gpg verify-tag "$TAG_AT_HEAD" >/dev/null 2>&1; then + rm -rf "$VERIFY_HOME" + notify "❌ Обновление отклонено: подпись релиза ${TAG_AT_HEAD} не прошла проверку. Возможна компрометация источника — версия НЕ установлена." + exit 1 + fi + rm -rf "$VERIFY_HOME" + echo "[update] release signature OK: ${TAG_AT_HEAD}" +else + echo "[update] no release-signing-key.asc pinned — skipping signature check (bootstrap)." +fi + mkdir -p data git rev-parse HEAD > data/update-completed diff --git a/vite.config.ts b/vite.config.ts deleted file mode 100644 index 63be2a0..0000000 --- a/vite.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import tailwindcss from '@tailwindcss/vite'; -import react from '@vitejs/plugin-react'; -import { defineConfig } from 'vite'; - -export default defineConfig({ - plugins: [react(), tailwindcss()], -});