diff --git a/.env-dev b/.env-dev index 0dbfc46e..f1d071a8 100644 --- a/.env-dev +++ b/.env-dev @@ -1,11 +1,13 @@ APP_SECRET_51802906=eNmBDWFYOm1Hl2aMIcbp APP_AUTH_KEY_51802906=b1aedf7ab1aedf7ab1aedf7a8fb2b8ac60bb1aeb1aedf7ad4ca7b89c13117db438121e0 CORS_ORIGIN=https://localhost:10888 -SKIP_AUTH=false +SKIP_AUTH=true POSTGRES_HOST=postgresql-dev -MODELS_URL=http://host.docker.internal:5000 +MODELS_URL=http://models-dev:5000 RAG_URL=http://host.docker.internal:5000 POSTGRES_USER=user POSTGRES_PASSWORD=user API_KEYS_5= +API_KEYS_120= +TG_TOKEN= IMAGES_API_KEY=* diff --git a/DEPLOY_NOW.md b/DEPLOY_NOW.md new file mode 100644 index 00000000..a7415fed --- /dev/null +++ b/DEPLOY_NOW.md @@ -0,0 +1,55 @@ +# 🚀 ДЕПЛОЙ СЕЙЧАС + +## ✅ Что сделано локально + +- Создана **одна чистая миграция** с всеми полями +- `init-db.sh` исправлен (использует `prisma migrate deploy`) +- Всё закоммичено и запушено в git + +## 🔥 НА СЕРВЕРЕ ВЫПОЛНИТЕ: + +```bash +# 1. Pull обновлений +cd /root/GPTutor # или ваш путь к проекту +git pull + +# 2. Удалить старую БД (если есть) +docker-compose -f docker-compose-prod.yaml down backend-prod +docker volume rm gptutor_backend-db 2>/dev/null || true + +# 3. Пересобрать и запустить +docker-compose -f docker-compose-prod.yaml build backend-prod +docker-compose -f docker-compose-prod.yaml up -d backend-prod + +# 4. Проверить логи +docker logs -f backend-prod +``` + +## ✅ Что должно быть в логах: + +``` +Database does not exist, creating and applying migrations... +Applying migration `20251009103248_init` +Migration status: All migrations applied ✅ +Starting application... +``` + +## 🎯 Проверка работы: + +```bash +# Загрузить файл +curl -X POST http://your-server/upload \ + -H "Authorization: Bearer vk1..." \ + -F "file=@test.docx" +``` + +**Ожидается:** +- ✅ Файл загружается +- ✅ Конвертируется в PDF +- ✅ НЕТ ошибок `column does not exist` +- ✅ НЕТ ошибок `table does not exist` + +## 🎉 Готово! + +После деплоя всё будет работать! + diff --git a/FINAL_DEPLOY_INSTRUCTIONS.md b/FINAL_DEPLOY_INSTRUCTIONS.md new file mode 100644 index 00000000..9c763099 --- /dev/null +++ b/FINAL_DEPLOY_INSTRUCTIONS.md @@ -0,0 +1,245 @@ +# 🚀 ФИНАЛЬНАЯ ИНСТРУКЦИЯ ПО ДЕПЛОЮ + +## ✅ Все изменения запушены в git! + +Коммиты: +1. `bff778f` - feat: single migration with file conversion and caching support +2. `64684bf` - feat: add VK Storage, document conversion, caching, and generated images support + +--- + +## 🔥 НА СЕРВЕРЕ ВЫПОЛНИТЕ (в точности): + +```bash +# 1. Перейти в директорию проекта +cd /root/GPTutor +# (или ваш путь к проекту) + +# 2. Скачать обновления +git pull + +# 3. Остановить backend +docker-compose -f docker-compose-prod.yaml stop backend-prod + +# 4. Удалить старый контейнер +docker-compose -f docker-compose-prod.yaml rm -f backend-prod + +# 5. ВАЖНО: Удалить старую БД (один раз) +docker volume rm gptutor_backend-db +# Если volume нет, игнорируйте ошибку + +# 6. Пересобрать БЕЗ кеша (ОБЯЗАТЕЛЬНО!) +docker-compose -f docker-compose-prod.yaml build --no-cache backend-prod + +# 7. Запустить +docker-compose -f docker-compose-prod.yaml up -d backend-prod + +# 8. Проверить логи +docker logs -f backend-prod +``` + +--- + +## ✅ В логах ДОЛЖНО быть: + +``` +Database does not exist, creating and applying migrations... +Prisma schema loaded from prisma/schema.prisma +Datasource "db": SQLite database "prod.db" at "file:/app/prisma/prod.db" + +Applying migration `20251009103248_init` + +The following migrations have been applied: + +migrations/ + └─ 20251009103248_init/ + └─ migration.sql + +Migration status: +Database schema is up to date! + +Starting application... +``` + +### ❌ НЕ должно быть: + +``` +No migration found in prisma/migrations ← Если это есть - плохо! +``` + +--- + +## 🧪 Проверка после деплоя + +### 1. Health Check + +```bash +curl http://localhost:3001/health +# Ожидаемо: {"status":"ok"} +``` + +### 2. Проверка миграций + +```bash +docker exec backend-prod npx prisma migrate status +# Ожидаемо: 1 migration found, Database schema is up to date! +``` + +### 3. Проверка структуры БД + +```bash +docker exec backend-prod sqlite3 /app/prisma/prod.db "PRAGMA table_info(files);" +# Должны быть поля: notStatic, originalName, originalSize, converted +``` + +### 4. Тест загрузки DOCX + +```bash +curl -X POST http://your-server/upload \ + -H "Authorization: Bearer vk1..." \ + -F "file=@test.docx" +``` + +**Ожидаемый ответ:** +```json +{ + "success": true, + "message": "File converted to PDF and uploaded successfully!", + "data": { + "converted": true, + "fromCache": false, + "file": { + "name": "test.pdf", + "type": "application/pdf" + } + } +} +``` + +### 5. Тест повторной загрузки (кеш) + +```bash +curl -X POST http://your-server/upload \ + -H "Authorization: Bearer vk1..." \ + -F "file=@test.docx" +``` + +**Ожидаемый ответ:** +```json +{ + "success": true, + "message": "File already converted and cached!", + "data": { + "converted": true, + "fromCache": true ← Работает из кеша! + } +} +``` + +--- + +## 🎨 Тест генерации изображений + +В приложении отправьте: +``` +Нарисуй красивый закат +``` + +Модель должна: +1. Вернуть текст +2. Вернуть изображение (base64) +3. Изображение отобразится в чате + +--- + +## 🐛 Если что-то пошло не так + +### Ошибка: "No migration found" + +**Причина:** Старый кеш Docker или миграции не скопировались + +**Решение:** +```bash +# Проверить что миграции есть на сервере +ls -la GPTutor-Backend-v2/prisma/migrations/ +# Должна быть папка: 20251009103248_init/ + +# Если её нет: +git pull --force + +# Пересобрать БЕЗ кеша +docker-compose -f docker-compose-prod.yaml build --no-cache backend-prod +``` + +### Ошибка: "column does not exist" + +**Причина:** Миграция не применилась + +**Решение:** +```bash +docker exec backend-prod npx prisma migrate deploy +docker restart backend-prod +``` + +### Ошибка: "table does not exist" + +**Причина:** БД не была создана + +**Решение:** +```bash +# Проверить что БД создалась +docker exec backend-prod ls -la /app/prisma/ +# Должен быть файл: prod.db + +# Если нет - проверить логи +docker logs backend-prod | grep -i error +``` + +--- + +## 📊 Итоговый статус + +| Компонент | Статус | +|-----------|--------| +| VK Storage (модель) | ✅ Готово | +| Конвертация DOC/DOCX/PPT/PPTX | ✅ Готово | +| Кеширование конвертаций | ✅ Готово | +| Сгенерированные изображения | ✅ Готово | +| Миграции Prisma | ✅ Исправлено | +| Dockerfile | ✅ Обновлен (LibreOffice) | +| Git | ✅ Все запушено | + +--- + +## 🎉 Готово к деплою! + +Просто выполните команды выше на сервере, и всё заработает. + +**Время выполнения:** ~3-5 минут + +**Важно:** Используйте `--no-cache` при сборке, чтобы миграции точно скопировались! + +--- + +## 📞 Нужна помощь? + +Отправьте логи: +```bash +docker logs backend-prod > logs.txt +``` + +И проверьте: +```bash +# Миграции на сервере +ls -la GPTutor-Backend-v2/prisma/migrations/ + +# Миграции в контейнере +docker exec backend-prod ls -la /app/prisma/migrations/ +``` + +Если в контейнере миграций нет - значит `build --no-cache` не был выполнен! + +--- + +Удачи! 🍀 + diff --git a/GPTutor-Backend-v2/.dockerignore b/GPTutor-Backend-v2/.dockerignore new file mode 100644 index 00000000..7f72dd54 --- /dev/null +++ b/GPTutor-Backend-v2/.dockerignore @@ -0,0 +1,12 @@ +node_modules +npm-debug.log +.git +.gitignore +README.md +.env +.nyc_output +coverage +.coverage +dist +*.db + diff --git a/GPTutor-Backend-v2/Dockerfile b/GPTutor-Backend-v2/Dockerfile new file mode 100644 index 00000000..4c74ef58 --- /dev/null +++ b/GPTutor-Backend-v2/Dockerfile @@ -0,0 +1,72 @@ +# Build stage +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy source code (includes prisma folder with schema and migrations) +COPY . . + +# Accept DATABASE_URL as build argument +ARG DATABASE_URL +ENV DATABASE_URL=$DATABASE_URL + +# Generate Prisma client +RUN npx prisma generate + +# Build TypeScript +RUN npm run build + +# Production stage +FROM node:20-alpine + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install production dependencies only +RUN npm ci --only=production + +# Copy Prisma schema and ALL migrations from builder stage +COPY --from=builder /app/prisma ./prisma + +# Generate Prisma client in production +RUN npx prisma generate + +# Install LibreOffice and dependencies for document conversion and PDF optimization +RUN apk update && \ + apk add --no-cache \ + ghostscript \ + libreoffice \ + libreoffice-writer \ + libreoffice-impress \ + ttf-dejavu \ + ttf-liberation \ + font-noto + +# Copy built application from builder +COPY --from=builder /app/dist ./dist + +# Copy initialization script +COPY init-db.sh ./ +RUN chmod +x init-db.sh + +# Create directories +RUN mkdir -p logs data + +# Expose port +EXPOSE 3001 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3001/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" + +# Start application with database initialization +CMD ["./init-db.sh"] + diff --git a/GPTutor-Backend-v2/dist/config/rateLimitConfig.d.ts b/GPTutor-Backend-v2/dist/config/rateLimitConfig.d.ts new file mode 100644 index 00000000..5bead3fe --- /dev/null +++ b/GPTutor-Backend-v2/dist/config/rateLimitConfig.d.ts @@ -0,0 +1,4 @@ +import { RateLimitOptions } from "../middleware/rateLimitMiddleware"; +export declare const RATE_LIMIT_CONFIG: RateLimitOptions; +export declare function getRateLimitConfigForEnv(): RateLimitOptions; +export declare function getRateLimitForRoute(route: string): import("../middleware/rateLimitMiddleware").RateLimitConfig; diff --git a/GPTutor-Backend-v2/dist/config/rateLimitConfig.js b/GPTutor-Backend-v2/dist/config/rateLimitConfig.js new file mode 100644 index 00000000..7f61ef61 --- /dev/null +++ b/GPTutor-Backend-v2/dist/config/rateLimitConfig.js @@ -0,0 +1,41 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.RATE_LIMIT_CONFIG = void 0; +exports.getRateLimitConfigForEnv = getRateLimitConfigForEnv; +exports.getRateLimitForRoute = getRateLimitForRoute; +exports.RATE_LIMIT_CONFIG = { + "/health": { + max: 100, + timeWindow: 60 * 1000, + }, + "/user": { + max: 30, + timeWindow: 60 * 1000, + }, + "/update-token": { + max: 5, + timeWindow: 60 * 1000, + }, + "/upload": { + max: 10, + timeWindow: 60 * 1000, + }, + "/v1/chat/completions": { + max: 500, + timeWindow: 60 * 1000, + }, + "/v1/models": { + max: 50, + timeWindow: 60 * 1000, + }, +}; +function getRateLimitConfigForEnv() { + return { ...exports.RATE_LIMIT_CONFIG }; +} +function getRateLimitForRoute(route) { + const config = getRateLimitConfigForEnv(); + return (config[route] || { + max: 30, + timeWindow: 60 * 1000, + }); +} diff --git a/GPTutor-Backend-v2/dist/controllers/AuthController.d.ts b/GPTutor-Backend-v2/dist/controllers/AuthController.d.ts new file mode 100644 index 00000000..7a1afced --- /dev/null +++ b/GPTutor-Backend-v2/dist/controllers/AuthController.d.ts @@ -0,0 +1,9 @@ +import { BaseController } from './BaseController'; +import { AuthService } from '../services/AuthService'; +export declare class AuthController extends BaseController { + private authService; + constructor(fastify: any, authService: AuthService); + registerRoutes(): void; + private getUser; + private updateToken; +} diff --git a/GPTutor-Backend-v2/dist/controllers/AuthController.js b/GPTutor-Backend-v2/dist/controllers/AuthController.js new file mode 100644 index 00000000..6b1365df --- /dev/null +++ b/GPTutor-Backend-v2/dist/controllers/AuthController.js @@ -0,0 +1,73 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.AuthController = void 0; +const BaseController_1 = require("./BaseController"); +const authMiddleware_1 = require("../middleware/authMiddleware"); +const rateLimitMiddleware_1 = require("../middleware/rateLimitMiddleware"); +class AuthController extends BaseController_1.BaseController { + constructor(fastify, authService) { + super(fastify); + this.authService = authService; + } + registerRoutes() { + const vkAuthMiddleware = (0, authMiddleware_1.createVkAuthMiddleware)(this.authService); + const userRateLimit = (0, rateLimitMiddleware_1.createRateLimitMiddleware)((0, rateLimitMiddleware_1.getRateLimitConfig)('/user')); + const updateTokenRateLimit = (0, rateLimitMiddleware_1.createRateLimitMiddleware)((0, rateLimitMiddleware_1.getRateLimitConfig)('/update-token')); + this.fastify.get('/user', { + preHandler: [userRateLimit, vkAuthMiddleware] + }, this.getUser.bind(this)); + this.fastify.post('/update-token', { + preHandler: [updateTokenRateLimit, vkAuthMiddleware] + }, this.updateToken.bind(this)); + } + async getUser(request, reply) { + this.logInfo('User data requested', { + vkId: request.dbUser.vkId, + balance: request.dbUser.balance + }, request); + return this.sendSuccess(reply, { + message: "User data retrieved successfully!", + vkData: request.vkUser, + user: { + id: request.dbUser.id, + vkId: request.dbUser.vkId, + balance: request.dbUser.balance, + apiKey: request.dbUser.apiKey, + isActive: request.dbUser.isActive, + createdAt: request.dbUser.createdAt, + updatedAt: request.dbUser.updatedAt, + }, + timestamp: new Date().toISOString(), + }); + } + async updateToken(request, reply) { + try { + this.logInfo('Token update requested', { + userId: request.dbUser.id, + vkId: request.dbUser.vkId + }, request); + const updatedUser = await this.authService.updateUserToken(request.dbUser.id); + this.logInfo('Token updated successfully', { + userId: updatedUser.id, + newApiKey: updatedUser.apiKey.substring(0, 10) + '...' + }, request); + return this.sendSuccess(reply, { + message: "API token updated successfully!", + newApiKey: updatedUser.apiKey, + user: { + id: updatedUser.id, + vkId: updatedUser.vkId, + balance: updatedUser.balance, + isActive: updatedUser.isActive, + updatedAt: updatedUser.updatedAt, + }, + timestamp: new Date().toISOString(), + }); + } + catch (error) { + this.logError('Token update failed', error, request); + return this.sendError(reply, 'Failed to update token', 500); + } + } +} +exports.AuthController = AuthController; diff --git a/GPTutor-Backend-v2/dist/controllers/BaseController.d.ts b/GPTutor-Backend-v2/dist/controllers/BaseController.d.ts new file mode 100644 index 00000000..c5719a67 --- /dev/null +++ b/GPTutor-Backend-v2/dist/controllers/BaseController.d.ts @@ -0,0 +1,17 @@ +import { FastifyInstance, FastifyReply } from 'fastify'; +import { RequestWithLogging } from '../middleware/loggingMiddleware'; +export declare abstract class BaseController { + protected fastify: FastifyInstance; + constructor(fastify: FastifyInstance); + abstract registerRoutes(): void; + protected sendSuccess(reply: FastifyReply, data: any, statusCode?: number): FastifyReply, unknown, import("fastify").FastifySchema, import("fastify").FastifyTypeProviderDefault, unknown>; + protected sendError(reply: FastifyReply, message: string, statusCode?: number, request?: RequestWithLogging): FastifyReply, unknown, import("fastify").FastifySchema, import("fastify").FastifyTypeProviderDefault, unknown>; + protected sendValidationError(reply: FastifyReply, message: string, request?: RequestWithLogging): FastifyReply, unknown, import("fastify").FastifySchema, import("fastify").FastifyTypeProviderDefault, unknown>; + protected sendUnauthorized(reply: FastifyReply, message?: string, request?: RequestWithLogging): FastifyReply, unknown, import("fastify").FastifySchema, import("fastify").FastifyTypeProviderDefault, unknown>; + protected sendNotFound(reply: FastifyReply, message?: string, request?: RequestWithLogging): FastifyReply, unknown, import("fastify").FastifySchema, import("fastify").FastifyTypeProviderDefault, unknown>; + protected sendInsufficientBalance(reply: FastifyReply, request?: RequestWithLogging): FastifyReply, unknown, import("fastify").FastifySchema, import("fastify").FastifyTypeProviderDefault, unknown>; + protected logInfo(message: string, meta?: any, request?: RequestWithLogging): void; + protected logError(message: string, error?: Error | any, meta?: any, request?: RequestWithLogging): void; + protected logWarn(message: string, meta?: any, request?: RequestWithLogging): void; + protected logDebug(message: string, meta?: any, request?: RequestWithLogging): void; +} diff --git a/GPTutor-Backend-v2/dist/controllers/BaseController.js b/GPTutor-Backend-v2/dist/controllers/BaseController.js new file mode 100644 index 00000000..4798a662 --- /dev/null +++ b/GPTutor-Backend-v2/dist/controllers/BaseController.js @@ -0,0 +1,65 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.BaseController = void 0; +const LoggerService_1 = require("../services/LoggerService"); +class BaseController { + constructor(fastify) { + this.fastify = fastify; + } + sendSuccess(reply, data, statusCode = 200) { + return reply.code(statusCode).send(data); + } + sendError(reply, message, statusCode = 500, request) { + if (request) { + LoggerService_1.logger.warn(`Response error: ${message}`, { + requestId: request.requestId, + userId: request.userId, + statusCode, + url: request.url, + method: request.method + }); + } + return reply.code(statusCode).send({ error: message }); + } + sendValidationError(reply, message, request) { + return this.sendError(reply, message, 400, request); + } + sendUnauthorized(reply, message = 'Unauthorized', request) { + return this.sendError(reply, message, 401, request); + } + sendNotFound(reply, message = 'Not found', request) { + return this.sendError(reply, message, 404, request); + } + sendInsufficientBalance(reply, request) { + return this.sendError(reply, 'Insufficient balance', 402, request); + } + logInfo(message, meta, request) { + LoggerService_1.logger.info(message, { + ...meta, + requestId: request?.requestId, + userId: request?.userId + }); + } + logError(message, error, meta, request) { + LoggerService_1.logger.error(message, error, { + ...meta, + requestId: request?.requestId, + userId: request?.userId + }); + } + logWarn(message, meta, request) { + LoggerService_1.logger.warn(message, { + ...meta, + requestId: request?.requestId, + userId: request?.userId + }); + } + logDebug(message, meta, request) { + LoggerService_1.logger.debug(message, { + ...meta, + requestId: request?.requestId, + userId: request?.userId + }); + } +} +exports.BaseController = BaseController; diff --git a/GPTutor-Backend-v2/dist/controllers/CompletionController.d.ts b/GPTutor-Backend-v2/dist/controllers/CompletionController.d.ts new file mode 100644 index 00000000..38ca9a94 --- /dev/null +++ b/GPTutor-Backend-v2/dist/controllers/CompletionController.d.ts @@ -0,0 +1,19 @@ +import { BaseController } from "./BaseController"; +import { UserRepository } from "../repositories/UserRepository"; +import { LLMCostEvaluate } from "../services/LLMCostEvaluate"; +import { OpenRouterService } from "../services/OpenRouterService"; +export declare class CompletionController extends BaseController { + private userRepository; + private llmCostService; + private openRouterService; + private vkSecretKey; + constructor(fastify: any, userRepository: UserRepository, llmCostService: LLMCostEvaluate, openRouterService: OpenRouterService, vkSecretKey?: string); + registerRoutes(): void; + private chatCompletions; + private handleStreamingResponse; + private handleNonStreamingResponse; + /** + * Проверяет наличие файлов в сообщениях + */ + private hasFilesInMessages; +} diff --git a/GPTutor-Backend-v2/dist/controllers/CompletionController.js b/GPTutor-Backend-v2/dist/controllers/CompletionController.js new file mode 100644 index 00000000..30922bfa --- /dev/null +++ b/GPTutor-Backend-v2/dist/controllers/CompletionController.js @@ -0,0 +1,237 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.CompletionController = void 0; +const BaseController_1 = require("./BaseController"); +const LoggerService_1 = require("../services/LoggerService"); +const vkAuth_1 = require("../utils/vkAuth"); +const rateLimitMiddleware_1 = require("../middleware/rateLimitMiddleware"); +class CompletionController extends BaseController_1.BaseController { + constructor(fastify, userRepository, llmCostService, openRouterService, vkSecretKey = process.env.VK_SECRET_KEY || "") { + super(fastify); + this.userRepository = userRepository; + this.llmCostService = llmCostService; + this.openRouterService = openRouterService; + this.vkSecretKey = vkSecretKey; + } + registerRoutes() { + const completionsRateLimit = (0, rateLimitMiddleware_1.createRateLimitMiddleware)((0, rateLimitMiddleware_1.getRateLimitConfig)("/v1/chat/completions")); + this.fastify.post("/v1/chat/completions", { preHandler: completionsRateLimit }, this.chatCompletions.bind(this)); + } + async chatCompletions(request, reply) { + try { + const authHeader = request.headers.authorization; + const authResult = await (0, vkAuth_1.authenticateUser)(authHeader, this.vkSecretKey, this.userRepository); + console.log({ authResult }); + if (!authResult) { + return this.sendUnauthorized(reply, "Invalid authentication. Use Bearer token (sk-...) or VK authorization.", request); + } + let user; + let userId; + if (authResult.authType === "api_key") { + user = authResult.user; + userId = user.id.toString(); + } + else if (authResult.authType === "vk") { + const vkData = authResult.user; + let dbUser = await this.userRepository.findByVkId(vkData.vk_user_id); + if (!dbUser) { + dbUser = await this.userRepository.create({ + vkId: vkData.vk_user_id, + isActive: true, + }); + } + user = dbUser; + userId = user.id.toString(); + } + else { + return this.sendUnauthorized(reply, "Unknown authentication type", request); + } + if (!user.isActive) { + return this.sendUnauthorized(reply, "User account is inactive", request); + } + // Проверяем баланс пользователя + if (user.balance <= 0) { + this.logInfo("Insufficient balance", { + userId: user.id, + balance: user.balance, + }, request); + return this.sendInsufficientBalance(reply, request); + } + console.log({ user }); + request.userId = userId; + const requestBody = request.body; + if (!requestBody.messages || !Array.isArray(requestBody.messages)) { + return this.sendValidationError(reply, "messages array is required", request); + } + const model = requestBody.model || "google/gemini-2.5-flash-lite"; + const hasFiles = this.hasFilesInMessages(requestBody.messages); + this.logInfo(`LLM request initiated`, { + model, + messagesCount: requestBody.messages.length, + stream: requestBody.stream || false, + hasFiles, + }, request); + //todo добавить отнимание баланса + const openRouterParams = { + model, + messages: requestBody.messages, + max_tokens: requestBody.max_tokens, + temperature: requestBody.temperature, + top_p: requestBody.top_p, + frequency_penalty: requestBody.frequency_penalty, + presence_penalty: requestBody.presence_penalty, + stop: requestBody.stop, + ...(hasFiles && { + plugins: [ + { + id: "file-parser", + pdf: { + engine: "native", + }, + }, + ], + }), + }; + if (requestBody.stream) { + return this.handleStreamingResponse(reply, user, model, openRouterParams, request); + } + return this.handleNonStreamingResponse(reply, user, openRouterParams, request); + } + catch (error) { + this.logError("Completion API error", error, {}, request); + if (error instanceof Error) { + if (error.message === "User not found") { + return this.sendUnauthorized(reply, "Invalid API key", request); + } + if (error.message === "Insufficient balance") { + return this.sendInsufficientBalance(reply, request); + } + } + return this.sendError(reply, "Internal server error", 500, request); + } + } + async handleStreamingResponse(reply, user, model, openRouterParams, request) { + reply.raw.setHeader("Cache-Control", "no-cache"); + reply.raw.setHeader("Connection", "keep-alive"); + reply.raw.setHeader("X-Accel-Buffering", "no"); + reply.raw.setHeader("Access-Control-Allow-Origin", "*"); + reply.raw.setHeader("Content-type", "text/event-stream"); + let totalCost = 0; + try { + LoggerService_1.logger.llmRequest(model, user.id.toString(), undefined, undefined, { + requestId: request.requestId, + stream: true, + }); + const streamStartTime = Date.now(); + const stream = await this.openRouterService.createCompletionStream({ + ...openRouterParams, + stream: true, + }); + let chunkCount = 0; + for await (const chunk of stream) { + chunkCount++; + if (chunk.usage) { + const responseUsage = chunk.usage; + console.log({ usage: chunk.usage }); + const cost = this.llmCostService.calculateCost(responseUsage?.cost); + totalCost = cost; + chunk.usage = { + prompt_tokens: responseUsage?.prompt_tokens, + completion_tokens: responseUsage?.completion_tokens, + total_tokens: responseUsage?.total_tokens, + cost, + }; + } + const chunkData = `data: ${JSON.stringify(chunk)}\n\n`; + reply.raw.write(chunkData); + if (chunkCount % 50 === 0) { + this.logDebug(`Streaming progress: ${chunkCount} chunks sent`, {}, request); + } + } + reply.raw.write("data: [DONE]\n\n"); + reply.raw.end(); + const streamDuration = Date.now() - streamStartTime; + LoggerService_1.logger.llmResponse(model, user.id.toString(), 0, totalCost, streamDuration, { + requestId: request.requestId, + chunks: chunkCount, + stream: true, + }); + await this.userRepository.decreaseBalance(user.id, totalCost); + return; + } + catch (streamError) { + this.logError("Stream error", streamError, { model }, request); + try { + reply.raw.write(`data: ${JSON.stringify({ + error: "Stream error", + message: streamError instanceof Error + ? streamError.message + : "Unknown error", + })}\n\n`); + reply.raw.write("data: [DONE]\n\n"); + reply.raw.end(); + } + catch (writeError) { + this.logError("Error writing error response to stream", writeError, {}, request); + } + return; + } + } + async handleNonStreamingResponse(reply, user, openRouterParams, request) { + const requestStartTime = Date.now(); + LoggerService_1.logger.llmRequest(openRouterParams.model, user.id.toString(), undefined, undefined, { + requestId: request.requestId, + stream: false, + }); + const completion = await this.openRouterService.createCompletion({ + ...openRouterParams, + stream: false, + }); + console.log(completion); + const responseUsage = completion.usage; + console.log({ usage: responseUsage }); + const originalCostUsd = completion.usage?.cost; + const cost = this.llmCostService.calculateCost(originalCostUsd); + const requestDuration = Date.now() - requestStartTime; + const totalTokens = completion.usage?.total_tokens || 0; + await this.userRepository.decreaseBalance(user.id, cost); + const updatedUser = (await this.userRepository.findByVkId(user.vkId || "")) || + (await this.userRepository.findByApiKey(request.headers.authorization?.substring(7) || "")); + LoggerService_1.logger.llmResponse(openRouterParams.model, user.id.toString(), totalTokens, cost, requestDuration, { + requestId: request.requestId, + originalCostUsd, + remainingBalance: updatedUser?.balance, + }); + LoggerService_1.logger.balance("decrease", user.id.toString(), cost, updatedUser?.balance || 0, { + requestId: request.requestId, + reason: "llm_completion", + }); + const responseWithCost = { + ...completion, + usage: { + prompt_tokens: responseUsage?.prompt_tokens, + completion_tokens: responseUsage?.completion_tokens, + total_tokens: responseUsage?.total_tokens, + cost, + }, + }; + return this.sendSuccess(reply, responseWithCost); + } + /** + * Проверяет наличие файлов в сообщениях + */ + hasFilesInMessages(messages) { + return messages.some((message) => { + // Если content это строка, файлов нет + if (typeof message.content === "string") { + return false; + } + // Если content это массив, проверяем каждый элемент + if (Array.isArray(message.content)) { + return message.content.some((contentItem) => contentItem.type === "image_url" || contentItem.type === "file"); + } + return false; + }); + } +} +exports.CompletionController = CompletionController; diff --git a/GPTutor-Backend-v2/dist/controllers/FilesController.d.ts b/GPTutor-Backend-v2/dist/controllers/FilesController.d.ts new file mode 100644 index 00000000..5026a60d --- /dev/null +++ b/GPTutor-Backend-v2/dist/controllers/FilesController.d.ts @@ -0,0 +1,12 @@ +import { BaseController } from "./BaseController"; +import { FilesService } from "../services/FilesService"; +import { FileRepository } from "../repositories/FileRepository"; +import { AuthService } from "../services/AuthService"; +export declare class FilesController extends BaseController { + private filesService; + private fileRepository; + private authService; + constructor(fastify: any, filesService: FilesService, fileRepository: FileRepository, authService: AuthService); + registerRoutes(): void; + private uploadFile; +} diff --git a/GPTutor-Backend-v2/dist/controllers/FilesController.js b/GPTutor-Backend-v2/dist/controllers/FilesController.js new file mode 100644 index 00000000..5a7f2e83 --- /dev/null +++ b/GPTutor-Backend-v2/dist/controllers/FilesController.js @@ -0,0 +1,155 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.FilesController = void 0; +const BaseController_1 = require("./BaseController"); +const authMiddleware_1 = require("../middleware/authMiddleware"); +const rateLimitMiddleware_1 = require("../middleware/rateLimitMiddleware"); +class FilesController extends BaseController_1.BaseController { + constructor(fastify, filesService, fileRepository, authService) { + super(fastify); + this.filesService = filesService; + this.fileRepository = fileRepository; + this.authService = authService; + } + registerRoutes() { + const vkAuthMiddleware = (0, authMiddleware_1.createVkAuthMiddleware)(this.authService); + const uploadRateLimit = (0, rateLimitMiddleware_1.createRateLimitMiddleware)((0, rateLimitMiddleware_1.getRateLimitConfig)("/upload")); + this.fastify.post("/upload", { + preHandler: [uploadRateLimit, vkAuthMiddleware], + config: { + bodyLimit: 50 * 1024 * 1024, + }, + }, this.uploadFile.bind(this)); + } + async uploadFile(request, reply) { + try { + this.logInfo("File upload requested", { + userId: request.dbUser.id, + vkId: request.dbUser.vkId, + }); + const data = await request.file(); + if (!data) { + return this.sendError(reply, "No file provided", 400, request); + } + const maxSize = 50 * 1024 * 1024; + if (data.file.bytesRead > maxSize) { + return this.sendError(reply, "File too large. Maximum size is 50MB", 413, request); + } + const fileBuffer = await data.toBuffer(); + const arrayBuffer = fileBuffer.buffer.slice(fileBuffer.byteOffset, fileBuffer.byteOffset + fileBuffer.byteLength); + this.logInfo("Processing file", { + fileName: data.filename, + fileSize: data.file.bytesRead, + mimeType: data.mimetype, + }); + const existingFile = await this.fileRepository.findByNameAndSizeOrOriginal(data.filename, data.file.bytesRead); + if (existingFile) { + this.logInfo("File already exists, returning cached file", { + fileId: existingFile.id, + fileName: existingFile.name, + url: existingFile.url, + wasConverted: existingFile.converted, + originalName: existingFile.originalName, + }); + return this.sendSuccess(reply, { + message: existingFile.converted + ? "File already converted and cached!" + : "File already exists!", + file: { + id: existingFile.id, + name: existingFile.name, + type: existingFile.type, + url: existingFile.url, + size: existingFile.size, + createdAt: existingFile.createdAt, + }, + converted: existingFile.converted, + fromCache: true, + timestamp: new Date().toISOString(), + }); + } + const result = await this.filesService.optimizeAndUploadFile(arrayBuffer, data.filename); + let finalMimeType = data.mimetype; + let finalFileName = data.filename; + let wasConverted = false; + if (result.finalFileName !== data.filename) { + wasConverted = true; + finalMimeType = "application/pdf"; + finalFileName = result.finalFileName; + this.logInfo("File was converted to PDF", { + originalName: data.filename, + originalType: data.mimetype, + originalSize: data.file.bytesRead, + finalName: finalFileName, + finalType: finalMimeType, + }); + } + // Сохраняем файл с информацией об оригинале (если был конвертирован) + const savedFile = await this.fileRepository.create({ + userId: request.dbUser.id, + type: finalMimeType, + name: finalFileName, + url: result.url, + size: data.file.bytesRead, // Сохраняем оригинальный размер для правильного кеширования + originalName: wasConverted ? data.filename : undefined, + originalSize: wasConverted ? data.file.bytesRead : undefined, + converted: wasConverted, + }); + this.logInfo("File uploaded successfully", { + fileId: savedFile.id, + fileName: savedFile.name, + url: savedFile.url, + userId: request.dbUser.id, + wasConverted: wasConverted, + originalName: savedFile.originalName, + originalSize: savedFile.originalSize, + }); + return this.sendSuccess(reply, { + message: wasConverted + ? "File converted to PDF and uploaded successfully!" + : "File uploaded successfully!", + file: { + id: savedFile.id, + name: savedFile.name, + type: savedFile.type, + url: savedFile.url, + size: savedFile.size, + createdAt: savedFile.createdAt, + }, + converted: wasConverted, + fromCache: false, + ...(wasConverted && { + originalFile: { + name: savedFile.originalName, + size: savedFile.originalSize, + }, + }), + timestamp: new Date().toISOString(), + }); + } + catch (error) { + this.logError("File upload failed", error); + if (error instanceof Error) { + const errorMessage = error.message; + if (errorMessage.includes("Unknown or unsupported file type")) { + return this.sendError(reply, "Unsupported file type", 400, request); + } + if (errorMessage.includes("Invalid filename")) { + return this.sendError(reply, "Invalid filename", 400, request); + } + if (errorMessage.includes("LibreOffice not found")) { + return this.sendError(reply, "Document conversion requires LibreOffice. Please contact administrator.", 503, request); + } + if (errorMessage.includes("Failed to read document") || + errorMessage.includes("corrupted")) { + return this.sendError(reply, "Failed to convert document. The file may be corrupted or password-protected.", 400, request); + } + if (errorMessage.includes("Failed to convert document to PDF")) { + return this.sendError(reply, "Failed to convert document to PDF. Please try again or use a different file.", 400, request); + } + } + return this.sendError(reply, "Failed to upload file", 500, request); + } + } +} +exports.FilesController = FilesController; diff --git a/GPTutor-Backend-v2/dist/controllers/HealthController.d.ts b/GPTutor-Backend-v2/dist/controllers/HealthController.d.ts new file mode 100644 index 00000000..66c1b5bf --- /dev/null +++ b/GPTutor-Backend-v2/dist/controllers/HealthController.d.ts @@ -0,0 +1,5 @@ +import { BaseController } from './BaseController'; +export declare class HealthController extends BaseController { + registerRoutes(): void; + private healthCheck; +} diff --git a/GPTutor-Backend-v2/dist/controllers/HealthController.js b/GPTutor-Backend-v2/dist/controllers/HealthController.js new file mode 100644 index 00000000..ff936e08 --- /dev/null +++ b/GPTutor-Backend-v2/dist/controllers/HealthController.js @@ -0,0 +1,18 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.HealthController = void 0; +const BaseController_1 = require("./BaseController"); +const rateLimitMiddleware_1 = require("../middleware/rateLimitMiddleware"); +class HealthController extends BaseController_1.BaseController { + registerRoutes() { + const healthRateLimit = (0, rateLimitMiddleware_1.createRateLimitMiddleware)((0, rateLimitMiddleware_1.getRateLimitConfig)('/health')); + this.fastify.get('/health', { preHandler: healthRateLimit }, this.healthCheck.bind(this)); + } + async healthCheck(request, reply) { + return this.sendSuccess(reply, { + status: "ok", + timestamp: new Date().toISOString() + }); + } +} +exports.HealthController = HealthController; diff --git a/GPTutor-Backend-v2/dist/controllers/ModelsController.d.ts b/GPTutor-Backend-v2/dist/controllers/ModelsController.d.ts new file mode 100644 index 00000000..4c90a49e --- /dev/null +++ b/GPTutor-Backend-v2/dist/controllers/ModelsController.d.ts @@ -0,0 +1,9 @@ +import { BaseController } from "./BaseController"; +import { LLMCostEvaluate } from "../services/LLMCostEvaluate"; +export declare class ModelsController extends BaseController { + protected fastify: any; + private llmCostService; + constructor(fastify: any, llmCostService: LLMCostEvaluate); + registerRoutes(): void; + private getModels; +} diff --git a/GPTutor-Backend-v2/dist/controllers/ModelsController.js b/GPTutor-Backend-v2/dist/controllers/ModelsController.js new file mode 100644 index 00000000..04131cab --- /dev/null +++ b/GPTutor-Backend-v2/dist/controllers/ModelsController.js @@ -0,0 +1,101 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ModelsController = void 0; +const BaseController_1 = require("./BaseController"); +const rateLimitMiddleware_1 = require("../middleware/rateLimitMiddleware"); +class ModelsController extends BaseController_1.BaseController { + constructor(fastify, llmCostService) { + super(fastify); + this.fastify = fastify; + this.llmCostService = llmCostService; + } + registerRoutes() { + // Rate limiting для Models endpoint + const modelsRateLimit = (0, rateLimitMiddleware_1.createRateLimitMiddleware)((0, rateLimitMiddleware_1.getRateLimitConfig)("/v1/models")); + this.fastify.get("/v1/models", { preHandler: modelsRateLimit }, this.getModels.bind(this)); + } + async getModels(request, reply) { + try { + this.logInfo("Getting popular provider Models", {}, request); + // Проверяем, что сервис инициализирован + const allModels = this.llmCostService.getAllModels(); + if (!allModels || allModels.length === 0) { + this.logWarn("LLM Cost Service not initialized or no Models loaded", {}, request); + return this.sendError(reply, "Models service not ready", 503, request); + } + const models = this.llmCostService.getPopularProviderModels(); + // Проверяем, что модели загружены корректно + if (!models || models.length === 0) { + this.logWarn("No Models found from popular providers", {}, request); + return this.sendSuccess(reply, { + success: true, + data: { + models: [], + total: 0, + providers: [ + "x-ai", + "deepseek", + "google", + "qwen", + "perplexity", + "mistralai", + "openai", + "anthropic", + ], + lastUpdated: new Date().toISOString(), + }, + }); + } + this.logInfo(`Found ${models.length} models from popular providers`, { + modelCount: models.length, + sampleModel: models[0] + ? { + id: models[0].id, + hasPricingRub: !!models[0].pricing_rub, + hasPrompt: !!models[0].pricing_rub?.prompt, + } + : null, + }, request); + const sortedModels = models.sort((a, b) => { + // Безопасное получение цены с проверкой на существование + const priceA = a.pricing_rub?.prompt || 0; + const priceB = b.pricing_rub?.prompt || 0; + // Сначала сравниваем по цене (самые дорогие сверху) + const priceDiff = priceB - priceA; + if (priceDiff !== 0) { + return priceDiff; + } + // Если цены равны, сравниваем по дате создания (самые новые сверху) + const createdA = a.created || 0; + const createdB = b.created || 0; + return createdB - createdA; + }); + this.logInfo(`Retrieved ${sortedModels.length} popular models (sorted by price and creation date)`, { + modelCount: sortedModels.length, + }, request); + return this.sendSuccess(reply, { + success: true, + data: { + models: sortedModels, + total: sortedModels.length, + providers: [ + "x-ai", + "deepseek", + "google", + "qwen", + "perplexity", + "mistralai", + "openai", + "anthropic", + ], + lastUpdated: new Date().toISOString(), + }, + }); + } + catch (error) { + this.logError("Failed to get Models", error, {}, request); + return this.sendError(reply, "Failed to retrieve Models", 500, request); + } + } +} +exports.ModelsController = ModelsController; diff --git a/GPTutor-Backend-v2/dist/controllers/UsageController.d.ts b/GPTutor-Backend-v2/dist/controllers/UsageController.d.ts new file mode 100644 index 00000000..204a0d76 --- /dev/null +++ b/GPTutor-Backend-v2/dist/controllers/UsageController.d.ts @@ -0,0 +1,12 @@ +import { BaseController } from "./BaseController"; +import { UsageRepository } from "../repositories/UsageRepository"; +import { UserRepository } from "../repositories/UserRepository"; +export declare class UsageController extends BaseController { + private usageRepository; + private userRepository; + private vkSecretKey; + constructor(fastify: any, usageRepository: UsageRepository, userRepository: UserRepository, vkSecretKey?: string); + registerRoutes(): void; + private getUsageList; + private getUsageStats; +} diff --git a/GPTutor-Backend-v2/dist/controllers/UsageController.js b/GPTutor-Backend-v2/dist/controllers/UsageController.js new file mode 100644 index 00000000..c4a466ef --- /dev/null +++ b/GPTutor-Backend-v2/dist/controllers/UsageController.js @@ -0,0 +1,125 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.UsageController = void 0; +const BaseController_1 = require("./BaseController"); +const universalAuthMiddleware_1 = require("../middleware/universalAuthMiddleware"); +class UsageController extends BaseController_1.BaseController { + constructor(fastify, usageRepository, userRepository, vkSecretKey = process.env.VK_SECRET_KEY || "") { + super(fastify); + this.usageRepository = usageRepository; + this.userRepository = userRepository; + this.vkSecretKey = vkSecretKey; + } + registerRoutes() { + const authMiddleware = (0, universalAuthMiddleware_1.createUniversalAuthMiddleware)(this.userRepository, this.vkSecretKey); + this.fastify.get("/v1/usage", { preHandler: authMiddleware }, this.getUsageList.bind(this)); + this.fastify.get("/v1/usage/stats", { preHandler: authMiddleware }, this.getUsageStats.bind(this)); + } + async getUsageList(request, reply) { + try { + const userId = (0, universalAuthMiddleware_1.getUserId)(request); + // Парсинг параметров + const query = request.query; + const page = parseInt(query.page || "1"); + const limit = Math.min(parseInt(query.limit || "20"), 100); // максимум 100 записей + const sortBy = query.sortBy || 'createdAt'; + const sortOrder = query.sortOrder || 'desc'; + // Фильтры - пользователь может видеть только свои записи + const filters = { + userId: userId, + }; + if (query.model) { + filters.model = query.model; + } + if (query.stream !== undefined) { + filters.stream = query.stream === 'true'; + } + if (query.startDate) { + filters.startDate = new Date(query.startDate); + } + if (query.endDate) { + filters.endDate = new Date(query.endDate); + } + const pagination = { + page, + limit, + sortBy, + sortOrder, + }; + const result = await this.usageRepository.findPaginated(filters, pagination); + this.logInfo(`Usage list requested`, { + userId, + page, + limit, + totalRecords: result.total, + filters, + }, request); + return this.sendSuccess(reply, { + records: result.records.map((record) => ({ + id: record.id, + model: record.model, + promptTokens: record.promptTokens, + completionTokens: record.completionTokens, + totalTokens: record.totalTokens, + costRub: record.costRub, + costUsd: record.costUsd, + duration: record.duration, + stream: record.stream, + requestId: record.requestId, + messagesCount: record.messagesCount, + temperature: record.temperature, + maxTokens: record.maxTokens, + topP: record.topP, + frequencyPenalty: record.frequencyPenalty, + presencePenalty: record.presencePenalty, + createdAt: record.createdAt, + user: record.user, + })), + pagination: { + page: result.page, + limit: result.limit, + total: result.total, + totalPages: result.totalPages, + }, + }); + } + catch (error) { + this.logError("Usage list error", error, {}, request); + if (error instanceof Error) { + if (error.message === "User not authenticated") { + return this.sendUnauthorized(reply, "Authentication required", request); + } + } + return this.sendError(reply, "Internal server error", 500, request); + } + } + async getUsageStats(request, reply) { + try { + const userId = (0, universalAuthMiddleware_1.getUserId)(request); + const query = request.query; + // Пользователь может видеть только свою статистику + const targetUserId = userId; + const startDate = query.startDate ? new Date(query.startDate) : undefined; + const endDate = query.endDate ? new Date(query.endDate) : undefined; + const stats = await this.usageRepository.getUsageStats(targetUserId, startDate, endDate); + this.logInfo(`Usage stats requested`, { + userId, + targetUserId, + startDate, + endDate, + totalRequests: stats.totalRequests, + }, request); + return this.sendSuccess(reply, stats); + } + catch (error) { + this.logError("Usage stats error", error, {}, request); + if (error instanceof Error) { + if (error.message === "User not authenticated") { + return this.sendUnauthorized(reply, "Authentication required", request); + } + } + return this.sendError(reply, "Internal server error", 500, request); + } + } +} +exports.UsageController = UsageController; diff --git a/GPTutor-Backend-v2/dist/controllers/index.d.ts b/GPTutor-Backend-v2/dist/controllers/index.d.ts new file mode 100644 index 00000000..e707731d --- /dev/null +++ b/GPTutor-Backend-v2/dist/controllers/index.d.ts @@ -0,0 +1,21 @@ +import { FastifyInstance } from 'fastify'; +import { AuthService } from '../services/AuthService'; +import { UserRepository } from '../repositories/UserRepository'; +import { FileRepository } from '../repositories/FileRepository'; +import { LLMCostEvaluate } from '../services/LLMCostEvaluate'; +import { OpenRouterService } from '../services/OpenRouterService'; +import { FilesService } from '../services/FilesService'; +export declare function registerControllers(fastify: FastifyInstance, dependencies: { + authService: AuthService; + userRepository: UserRepository; + fileRepository: FileRepository; + filesService: FilesService; + llmCostService: LLMCostEvaluate; + openRouterService: OpenRouterService; +}): void; +export * from './BaseController'; +export * from './HealthController'; +export * from './AuthController'; +export * from './CompletionController'; +export * from './ModelsController'; +export * from './FilesController'; diff --git a/GPTutor-Backend-v2/dist/controllers/index.js b/GPTutor-Backend-v2/dist/controllers/index.js new file mode 100644 index 00000000..856c84bb --- /dev/null +++ b/GPTutor-Backend-v2/dist/controllers/index.js @@ -0,0 +1,38 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.registerControllers = registerControllers; +const HealthController_1 = require("./HealthController"); +const AuthController_1 = require("./AuthController"); +const CompletionController_1 = require("./CompletionController"); +const ModelsController_1 = require("./ModelsController"); +const FilesController_1 = require("./FilesController"); +function registerControllers(fastify, dependencies) { + const controllers = [ + new HealthController_1.HealthController(fastify), + new AuthController_1.AuthController(fastify, dependencies.authService), + new CompletionController_1.CompletionController(fastify, dependencies.userRepository, dependencies.llmCostService, dependencies.openRouterService), + new ModelsController_1.ModelsController(fastify, dependencies.llmCostService), + new FilesController_1.FilesController(fastify, dependencies.filesService, dependencies.fileRepository, dependencies.authService), + ]; + controllers.forEach(controller => controller.registerRoutes()); +} +__exportStar(require("./BaseController"), exports); +__exportStar(require("./HealthController"), exports); +__exportStar(require("./AuthController"), exports); +__exportStar(require("./CompletionController"), exports); +__exportStar(require("./ModelsController"), exports); +__exportStar(require("./FilesController"), exports); diff --git a/GPTutor-Backend-v2/dist/index.d.ts b/GPTutor-Backend-v2/dist/index.d.ts new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/GPTutor-Backend-v2/dist/index.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/GPTutor-Backend-v2/dist/index.js b/GPTutor-Backend-v2/dist/index.js new file mode 100644 index 00000000..9060e870 --- /dev/null +++ b/GPTutor-Backend-v2/dist/index.js @@ -0,0 +1,154 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const fastify_1 = __importDefault(require("fastify")); +const client_1 = require("@prisma/client"); +const UserRepository_1 = require("./repositories/UserRepository"); +const FileRepository_1 = require("./repositories/FileRepository"); +const AuthService_1 = require("./services/AuthService"); +const FilesService_1 = require("./services/FilesService"); +const FileCleanupService_1 = require("./services/FileCleanupService"); +const LLMCostEvaluate_1 = require("./services/LLMCostEvaluate"); +const OpenRouterService_1 = require("./services/OpenRouterService"); +const LoggerService_1 = require("./services/LoggerService"); +const controllers_1 = require("./controllers"); +const prisma = new client_1.PrismaClient(); +const userRepository = new UserRepository_1.UserRepository(prisma); +const fileRepository = new FileRepository_1.FileRepository(prisma); +console.log(process.env); +const authService = new AuthService_1.AuthService(userRepository, process.env.VK_APP_ID, process.env.VK_SECRET_KEY); +const filesService = new FilesService_1.FilesService(); +const fileCleanupService = new FileCleanupService_1.FileCleanupService(prisma, filesService); +const llmCostService = new LLMCostEvaluate_1.LLMCostEvaluate(100); +const openRouterService = new OpenRouterService_1.OpenRouterService(process.env.OPENROUTER_API_KEY); +const fastify = (0, fastify_1.default)({ + logger: true, + disableRequestLogging: true, +}); +fastify.register(require("@fastify/cors"), { + origin: "*", + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allowedHeaders: [ + "Content-Type", + "Authorization", + "Accept", + "Cache-Control", + "Pragma", + ], + credentials: true, + optionsSuccessStatus: 200, +}); +fastify.register(require("@fastify/helmet")); +fastify.register(require("@fastify/rate-limit"), { + max: 100, + timeWindow: 60 * 1000, + keyGenerator: (request) => { + const ip = request.ip; + const userId = request.userId || "anonymous"; + return `rate_limit:${ip}:${userId}`; + }, + errorResponseBuilder: (request, context) => { + return { + error: "Too Many Requests", + message: `Rate limit exceeded. Maximum ${context.max} requests per ${context.timeWindow / 1000} seconds.`, + retryAfter: Math.ceil(context.after / 1000), + }; + }, +}); +fastify.register(require("@fastify/multipart"), { + limits: { + fileSize: 50 * 1024 * 1024, + }, +}); +fastify.addHook("preHandler", async (request, reply) => { + request.requestId = require("uuid").v4(); + request.startTime = Date.now(); + LoggerService_1.logger.apiRequest(request.method, request.url, request.userId, { + requestId: request.requestId, + userAgent: request.headers["user-agent"], + ip: request.ip, + }); +}); +fastify.addHook("onResponse", async (request, reply) => { + const duration = Date.now() - (request.startTime || 0); + LoggerService_1.logger.apiResponse(request.method, request.url, reply.statusCode, duration, request.userId, { + requestId: request.requestId, + }); +}); +fastify.setErrorHandler(async (error, request, reply) => { + const duration = Date.now() - (request.startTime || 0); + LoggerService_1.logger.error(`Request failed: ${request.method} ${request.url}`, error, { + requestId: request.requestId, + userId: request.userId, + duration, + statusCode: reply.statusCode, + }); + throw error; +}); +(0, controllers_1.registerControllers)(fastify, { + authService, + userRepository, + fileRepository, + filesService, + llmCostService, + openRouterService, +}); +const start = async () => { + try { + LoggerService_1.logger.info("Starting GPTutor Backend v2..."); + await llmCostService.initialize(); + fileCleanupService.start(); + await fastify.listen({ + port: Number(process.env.PORT) || 3001, + host: "0.0.0.0", + }); + LoggerService_1.logger.info("🚀 Server is running", { + port: Number(process.env.PORT) || 3001, + host: "0.0.0.0", + environment: process.env.NODE_ENV || "development", + }); + } + catch (err) { + LoggerService_1.logger.error("Failed to start server", err); + process.exit(1); + } +}; +process.on("SIGINT", async () => { + LoggerService_1.logger.info("Received SIGINT, shutting down gracefully..."); + try { + fileCleanupService.stop(); + await fastify.close(); + await prisma.$disconnect(); + LoggerService_1.logger.info("Server shut down successfully"); + process.exit(0); + } + catch (error) { + LoggerService_1.logger.error("Error during shutdown", error); + process.exit(1); + } +}); +process.on("SIGTERM", async () => { + LoggerService_1.logger.info("Received SIGTERM, shutting down gracefully..."); + try { + fileCleanupService.stop(); + await fastify.close(); + await prisma.$disconnect(); + LoggerService_1.logger.info("Server shut down successfully"); + process.exit(0); + } + catch (error) { + LoggerService_1.logger.error("Error during shutdown", error); + process.exit(1); + } +}); +process.on("uncaughtException", (error) => { + LoggerService_1.logger.error("Uncaught Exception", error); + process.exit(1); +}); +process.on("unhandledRejection", (reason, promise) => { + LoggerService_1.logger.error("Unhandled Rejection", reason, { promise }); + process.exit(1); +}); +start(); diff --git a/GPTutor-Backend-v2/dist/middleware/authMiddleware.d.ts b/GPTutor-Backend-v2/dist/middleware/authMiddleware.d.ts new file mode 100644 index 00000000..96c79837 --- /dev/null +++ b/GPTutor-Backend-v2/dist/middleware/authMiddleware.d.ts @@ -0,0 +1,4 @@ +import { FastifyReply } from "fastify"; +import { AuthService } from "../services/AuthService"; +import { RequestWithLogging } from "./loggingMiddleware"; +export declare function createVkAuthMiddleware(authService: AuthService): (request: RequestWithLogging, reply: FastifyReply) => Promise; diff --git a/GPTutor-Backend-v2/dist/middleware/authMiddleware.js b/GPTutor-Backend-v2/dist/middleware/authMiddleware.js new file mode 100644 index 00000000..408b5acd --- /dev/null +++ b/GPTutor-Backend-v2/dist/middleware/authMiddleware.js @@ -0,0 +1,36 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createVkAuthMiddleware = createVkAuthMiddleware; +const LoggerService_1 = require("../services/LoggerService"); +function createVkAuthMiddleware(authService) { + return async function vkAuthMiddleware(request, reply) { + const authHeader = request.headers.authorization; + if (!authHeader || !authHeader.startsWith("Bearer ")) { + reply + .code(401) + .send({ error: "Missing or invalid authorization header" }); + return; + } + const token = authHeader.substring(7); // Remove "Bearer " + try { + const authResult = await authService.authorizeVKUser(token); + request.vkUser = authResult.vkData; + request.dbUser = authResult.dbUser; + request.userId = authResult.dbUser.id.toString(); + LoggerService_1.logger.auth("vk_authorize", request.userId, true, { + requestId: request.requestId, + vkId: authResult.vkData.id, + }); + } + catch (error) { + LoggerService_1.logger.auth("vk_authorize", request.userId, false, { + requestId: request.requestId, + error: error instanceof Error ? error.message : "Unknown error", + }); + reply.code(401).send({ + error: error instanceof Error ? error.message : "Authorization failed", + }); + return; + } + }; +} diff --git a/GPTutor-Backend-v2/dist/middleware/loggingMiddleware.d.ts b/GPTutor-Backend-v2/dist/middleware/loggingMiddleware.d.ts new file mode 100644 index 00000000..f72f7c35 --- /dev/null +++ b/GPTutor-Backend-v2/dist/middleware/loggingMiddleware.d.ts @@ -0,0 +1,8 @@ +import { FastifyRequest, FastifyReply } from 'fastify'; +export interface RequestWithLogging extends FastifyRequest { + requestId: string; + startTime: number; + userId?: string; +} +export declare function createLoggingMiddleware(): (request: RequestWithLogging, reply: FastifyReply) => Promise; +export declare function createErrorLoggingMiddleware(): (error: Error, request: RequestWithLogging, reply: FastifyReply) => Promise; diff --git a/GPTutor-Backend-v2/dist/middleware/loggingMiddleware.js b/GPTutor-Backend-v2/dist/middleware/loggingMiddleware.js new file mode 100644 index 00000000..f55efa56 --- /dev/null +++ b/GPTutor-Backend-v2/dist/middleware/loggingMiddleware.js @@ -0,0 +1,48 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createLoggingMiddleware = createLoggingMiddleware; +exports.createErrorLoggingMiddleware = createErrorLoggingMiddleware; +const LoggerService_1 = require("../services/LoggerService"); +const uuid_1 = require("uuid"); +function createLoggingMiddleware() { + return async function loggingMiddleware(request, reply) { + // Generate unique request ID + request.requestId = (0, uuid_1.v4)(); + request.startTime = Date.now(); + // Try to extract user ID from various sources + const authHeader = request.headers.authorization; + if (authHeader && authHeader.startsWith('Bearer ')) { + // This will be set by auth middleware later, but we can try to extract it + request.userId = 'unknown'; // Will be updated by auth middleware + } + // Log incoming request + LoggerService_1.logger.apiRequest(request.method, request.url, request.userId, { + requestId: request.requestId, + userAgent: request.headers['user-agent'], + ip: request.ip, + headers: { + 'content-type': request.headers['content-type'], + 'authorization': authHeader ? 'Bearer [REDACTED]' : undefined + } + }); + // We'll log response in the main request handler instead + // since Fastify's hook system has complex typing + // Add request ID to response headers for tracking + reply.header('X-Request-ID', request.requestId); + }; +} +function createErrorLoggingMiddleware() { + return async function errorLoggingMiddleware(error, request, reply) { + const duration = Date.now() - request.startTime; + LoggerService_1.logger.error(`Request failed: ${request.method} ${request.url}`, error, { + requestId: request.requestId, + userId: request.userId, + duration, + statusCode: reply.statusCode, + userAgent: request.headers['user-agent'], + ip: request.ip + }); + // Don't modify the error, just log it + throw error; + }; +} diff --git a/GPTutor-Backend-v2/dist/middleware/rateLimitMiddleware.d.ts b/GPTutor-Backend-v2/dist/middleware/rateLimitMiddleware.d.ts new file mode 100644 index 00000000..4fb91475 --- /dev/null +++ b/GPTutor-Backend-v2/dist/middleware/rateLimitMiddleware.d.ts @@ -0,0 +1,24 @@ +import { FastifyRequest, FastifyReply } from "fastify"; +export interface RateLimitConfig { + max: number; + timeWindow: number; + keyGenerator?: (request: FastifyRequest) => string; + onLimitReached?: (request: FastifyRequest, reply: FastifyReply) => void; + skipSuccessfulRequests?: boolean; + skipFailedRequests?: boolean; +} +export interface RateLimitOptions { + [route: string]: RateLimitConfig; +} +export declare function generateRateLimitKey(request: FastifyRequest): string; +export declare function generateIPRateLimitKey(request: FastifyRequest): string; +export declare function createRateLimitMiddleware(config: RateLimitConfig): (request: FastifyRequest, reply: FastifyReply) => Promise; +export declare function getRateLimitConfig(route: string): RateLimitConfig | null; +export declare function cleanupRateLimitStore(store: Map): void; +export declare function getGlobalRateLimitStore(): Map; diff --git a/GPTutor-Backend-v2/dist/middleware/rateLimitMiddleware.js b/GPTutor-Backend-v2/dist/middleware/rateLimitMiddleware.js new file mode 100644 index 00000000..5ebed4e8 --- /dev/null +++ b/GPTutor-Backend-v2/dist/middleware/rateLimitMiddleware.js @@ -0,0 +1,90 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.generateRateLimitKey = generateRateLimitKey; +exports.generateIPRateLimitKey = generateIPRateLimitKey; +exports.createRateLimitMiddleware = createRateLimitMiddleware; +exports.getRateLimitConfig = getRateLimitConfig; +exports.cleanupRateLimitStore = cleanupRateLimitStore; +exports.getGlobalRateLimitStore = getGlobalRateLimitStore; +const LoggerService_1 = require("../services/LoggerService"); +const rateLimitConfig_1 = require("../config/rateLimitConfig"); +function generateRateLimitKey(request) { + const ip = request.ip; + const userId = request.userId || "anonymous"; + const userAgent = request.headers["user-agent"] || "unknown"; + return `rate_limit:${ip}:${userId}:${Buffer.from(userAgent) + .toString("base64") + .slice(0, 10)}`; +} +function generateIPRateLimitKey(request) { + const ip = request.ip; + return `rate_limit:${ip}`; +} +const globalRateLimitStore = new Map(); +function createRateLimitMiddleware(config) { + const store = globalRateLimitStore; + return async (request, reply) => { + const key = config.keyGenerator + ? config.keyGenerator(request) + : generateIPRateLimitKey(request); + const now = Date.now(); + let record = store.get(key); + if (!record || now > record.resetTime) { + record = { + count: 0, + resetTime: now + config.timeWindow, + }; + store.set(key, record); + } + record.count++; + if (record.count > config.max) { + LoggerService_1.logger.warn("Rate limit exceeded", { + ip: request.ip, + url: request.url, + method: request.method, + key, + count: record.count, + max: config.max, + timeWindow: config.timeWindow, + }); + if (config.onLimitReached) { + config.onLimitReached(request, reply); + } + else { + reply.code(429).send({ + error: "Too Many Requests", + message: `Rate limit exceeded. Maximum ${config.max} requests per ${config.timeWindow / 1000} seconds.`, + retryAfter: Math.ceil((record.resetTime - now) / 1000), + }); + } + return; + } + reply.header("X-RateLimit-Limit", config.max.toString()); + reply.header("X-RateLimit-Remaining", Math.max(0, config.max - record.count).toString()); + reply.header("X-RateLimit-Reset", Math.ceil(record.resetTime / 1000).toString()); + store.set(key, record); + }; +} +function getRateLimitConfig(route) { + const env = process.env.NODE_ENV || "production"; + return (0, rateLimitConfig_1.getRateLimitForRoute)(route); +} +function cleanupRateLimitStore(store) { + const now = Date.now(); + let cleanedCount = 0; + for (const [key, record] of store.entries()) { + if (now > record.resetTime) { + store.delete(key); + cleanedCount++; + } + } + if (cleanedCount > 0) { + LoggerService_1.logger.debug(`Cleaned up ${cleanedCount} expired rate limit records`); + } +} +function getGlobalRateLimitStore() { + return globalRateLimitStore; +} +setInterval(() => { + cleanupRateLimitStore(globalRateLimitStore); +}, 5 * 60 * 1000); diff --git a/GPTutor-Backend-v2/dist/middleware/rateLimitMiddleware.test.d.ts b/GPTutor-Backend-v2/dist/middleware/rateLimitMiddleware.test.d.ts new file mode 100644 index 00000000..8902e934 --- /dev/null +++ b/GPTutor-Backend-v2/dist/middleware/rateLimitMiddleware.test.d.ts @@ -0,0 +1,2 @@ +declare function testRateLimitMiddleware(): Promise; +export { testRateLimitMiddleware }; diff --git a/GPTutor-Backend-v2/dist/middleware/rateLimitMiddleware.test.js b/GPTutor-Backend-v2/dist/middleware/rateLimitMiddleware.test.js new file mode 100644 index 00000000..90c8ba1e --- /dev/null +++ b/GPTutor-Backend-v2/dist/middleware/rateLimitMiddleware.test.js @@ -0,0 +1,60 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.testRateLimitMiddleware = testRateLimitMiddleware; +const rateLimitMiddleware_1 = require("./rateLimitMiddleware"); +// Простой тест для проверки работы rate limiting +async function testRateLimitMiddleware() { + console.log('🧪 Тестирование rate limiting middleware...'); + const config = { + max: 3, + timeWindow: 1000, // 1 секунда для быстрого тестирования + }; + const middleware = (0, rateLimitMiddleware_1.createRateLimitMiddleware)(config); + // Создаем мок объекты + const mockRequest = { + ip: '127.0.0.1', + url: '/test', + method: 'GET', + headers: { 'user-agent': 'test-agent' } + }; + const mockReply = { + code: (status) => ({ + send: (data) => { + console.log(`Response ${status}:`, data); + return { status, data }; + } + }), + header: (name, value) => { + console.log(`Header ${name}: ${value}`); + } + }; + console.log('\n📊 Тестирование нормальных запросов...'); + // Тест 1-3: нормальные запросы + for (let i = 1; i <= 3; i++) { + console.log(`Запрос ${i}:`); + await middleware(mockRequest, mockReply); + } + console.log('\n🚫 Тестирование превышения лимита...'); + // Тест 4: превышение лимита + console.log('Запрос 4 (должен быть заблокирован):'); + await middleware(mockRequest, mockReply); + console.log('\n🧹 Тестирование очистки store...'); + // Проверяем размер store до очистки + const store = (0, rateLimitMiddleware_1.getGlobalRateLimitStore)(); + console.log(`Store size before cleanup: ${store.size}`); + // Ждем истечения времени окна + console.log('Ожидание истечения timeWindow...'); + await new Promise(resolve => setTimeout(resolve, 1100)); + // Очищаем store + (0, rateLimitMiddleware_1.cleanupRateLimitStore)(store); + console.log(`Store size after cleanup: ${store.size}`); + console.log('\n✅ Тест после очистки...'); + // Тест после очистки - должен работать снова + console.log('Запрос после очистки:'); + await middleware(mockRequest, mockReply); + console.log('\n🎉 Тестирование завершено!'); +} +// Запуск теста, если файл выполняется напрямую +if (require.main === module) { + testRateLimitMiddleware().catch(console.error); +} diff --git a/GPTutor-Backend-v2/dist/middleware/universalAuthMiddleware.d.ts b/GPTutor-Backend-v2/dist/middleware/universalAuthMiddleware.d.ts new file mode 100644 index 00000000..a4ad0747 --- /dev/null +++ b/GPTutor-Backend-v2/dist/middleware/universalAuthMiddleware.d.ts @@ -0,0 +1,22 @@ +import { FastifyRequest, FastifyReply } from "fastify"; +import { UserRepository } from "../repositories/UserRepository"; +export interface AuthenticatedUser { + id: string; + username?: string | null; + email?: string | null; + vkId?: string | null; + balance: number; + apiKey: string; + isActive: boolean; + createdAt: Date; + updatedAt: Date; +} +export interface AuthenticatedRequest extends FastifyRequest { + user?: AuthenticatedUser; + userId?: string; + requestId?: string; + startTime?: number; +} +export declare function createUniversalAuthMiddleware(userRepository: UserRepository, vkSecretKey: string): (request: AuthenticatedRequest, reply: FastifyReply) => Promise; +export declare function requireAuth(request: AuthenticatedRequest): AuthenticatedUser; +export declare function getUserId(request: AuthenticatedRequest): string; diff --git a/GPTutor-Backend-v2/dist/middleware/universalAuthMiddleware.js b/GPTutor-Backend-v2/dist/middleware/universalAuthMiddleware.js new file mode 100644 index 00000000..e71b17d9 --- /dev/null +++ b/GPTutor-Backend-v2/dist/middleware/universalAuthMiddleware.js @@ -0,0 +1,95 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createUniversalAuthMiddleware = createUniversalAuthMiddleware; +exports.requireAuth = requireAuth; +exports.getUserId = getUserId; +const vkAuth_1 = require("../utils/vkAuth"); +const LoggerService_1 = require("../services/LoggerService"); +function createUniversalAuthMiddleware(userRepository, vkSecretKey) { + return async function universalAuthMiddleware(request, reply) { + try { + const authHeader = request.headers.authorization; + if (!authHeader) { + return reply.code(401).send({ + error: "Unauthorized", + message: "Missing authorization header", + }); + } + const authResult = await (0, vkAuth_1.authenticateUser)(authHeader, vkSecretKey, userRepository); + if (!authResult) { + return reply.code(401).send({ + error: "Unauthorized", + message: "Invalid authentication. Use Bearer token (sk-...) or VK authorization.", + }); + } + let user; + let userId; + if (authResult.authType === "api_key") { + user = authResult.user; + userId = user.id.toString(); + } + else if (authResult.authType === "vk") { + const vkData = authResult.user; + let dbUser = await userRepository.findByVkId(vkData.vk_user_id); + if (!dbUser) { + dbUser = await userRepository.create({ + vkId: vkData.vk_user_id, + isActive: true, + }); + } + user = dbUser; + userId = user.id.toString(); + } + else { + return reply.code(401).send({ + error: "Unauthorized", + message: "Unknown authentication type", + }); + } + if (!user.isActive) { + return reply.code(401).send({ + error: "Unauthorized", + message: "User account is inactive", + }); + } + // Добавляем пользователя в request + request.user = user; + request.userId = userId; + // Логируем успешную аутентификацию + LoggerService_1.logger.auth(authResult.authType, userId, true, { + requestId: request.requestId, + authType: authResult.authType, + userAgent: request.headers["user-agent"], + ip: request.ip, + }); + return; // Продолжаем выполнение + } + catch (error) { + // Логируем ошибку аутентификации + LoggerService_1.logger.auth("unknown", "anonymous", false, { + requestId: request.requestId, + error: error instanceof Error ? error.message : "Unknown error", + userAgent: request.headers["user-agent"], + ip: request.ip, + }); + return reply.code(401).send({ + error: "Unauthorized", + message: "Authentication failed", + }); + } + }; +} +// Вспомогательная функция для проверки, что пользователь аутентифицирован +function requireAuth(request) { + if (!request.user) { + throw new Error("User not authenticated"); + } + return request.user; +} +// Вспомогательная функция для получения ID пользователя +function getUserId(request) { + if (!request.userId) { + throw new Error("User not authenticated"); + } + return request.userId; +} diff --git a/GPTutor-Backend-v2/dist/repositories/FileRepository.d.ts b/GPTutor-Backend-v2/dist/repositories/FileRepository.d.ts new file mode 100644 index 00000000..2d1df98b --- /dev/null +++ b/GPTutor-Backend-v2/dist/repositories/FileRepository.d.ts @@ -0,0 +1,161 @@ +import { PrismaClient } from "@prisma/client"; +export declare class FileRepository { + private prisma; + constructor(prisma: PrismaClient); + create(data: { + userId: string; + type: string; + name: string; + url: string; + size: number; + originalName?: string; + originalSize?: number; + converted?: boolean; + }): Promise<{ + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + type: string; + url: string; + size: number; + notStatic: boolean; + originalName: string | null; + originalSize: number | null; + converted: boolean; + userId: string; + }>; + findById(id: string): Promise<{ + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + type: string; + url: string; + size: number; + notStatic: boolean; + originalName: string | null; + originalSize: number | null; + converted: boolean; + userId: string; + } | null>; + findByUserId(userId: string): Promise<{ + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + type: string; + url: string; + size: number; + notStatic: boolean; + originalName: string | null; + originalSize: number | null; + converted: boolean; + userId: string; + }[]>; + delete(id: string): Promise; + update(id: string, data: { + name?: string; + type?: string; + url?: string; + size?: number; + }): Promise<{ + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + type: string; + url: string; + size: number; + notStatic: boolean; + originalName: string | null; + originalSize: number | null; + converted: boolean; + userId: string; + }>; + findByUrl(url: string): Promise<{ + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + type: string; + url: string; + size: number; + notStatic: boolean; + originalName: string | null; + originalSize: number | null; + converted: boolean; + userId: string; + } | null>; + getFileStats(userId: string): Promise<{ + totalFiles: number; + totalSize: number; + filesByType: Record; + }>; + /** + * Находит файлы старше указанной даты + */ + findOlderThan(date: Date): Promise<{ + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + type: string; + url: string; + size: number; + notStatic: boolean; + originalName: string | null; + originalSize: number | null; + converted: boolean; + userId: string; + }[]>; + /** + * Находит файл по названию и размеру + */ + findByNameAndSize(name: string, size: number): Promise<{ + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + type: string; + url: string; + size: number; + notStatic: boolean; + originalName: string | null; + originalSize: number | null; + converted: boolean; + userId: string; + } | null>; + /** + * Находит файл по оригинальному названию и размеру + * Используется для поиска конвертированных файлов по их оригинальным данным + */ + findByOriginalNameAndSize(originalName: string, originalSize: number): Promise<{ + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + type: string; + url: string; + size: number; + notStatic: boolean; + originalName: string | null; + originalSize: number | null; + converted: boolean; + userId: string; + } | null>; + findByNameAndSizeOrOriginal(name: string, size: number): Promise<{ + name: string; + id: string; + createdAt: Date; + updatedAt: Date; + type: string; + url: string; + size: number; + notStatic: boolean; + originalName: string | null; + originalSize: number | null; + converted: boolean; + userId: string; + } | null>; +} diff --git a/GPTutor-Backend-v2/dist/repositories/FileRepository.js b/GPTutor-Backend-v2/dist/repositories/FileRepository.js new file mode 100644 index 00000000..57365cdc --- /dev/null +++ b/GPTutor-Backend-v2/dist/repositories/FileRepository.js @@ -0,0 +1,120 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.FileRepository = void 0; +class FileRepository { + constructor(prisma) { + this.prisma = prisma; + } + async create(data) { + return this.prisma.file.create({ + data: { + userId: data.userId, + type: data.type, + name: data.name, + url: data.url, + size: data.size, + originalName: data.originalName, + originalSize: data.originalSize, + converted: data.converted || false, + }, + }); + } + async findById(id) { + return this.prisma.file.findUnique({ + where: { id }, + }); + } + async findByUserId(userId) { + return this.prisma.file.findMany({ + where: { userId }, + orderBy: { createdAt: "desc" }, + }); + } + async delete(id) { + await this.prisma.file.delete({ + where: { id }, + }); + } + async update(id, data) { + return this.prisma.file.update({ + where: { id }, + data, + }); + } + async findByUrl(url) { + return this.prisma.file.findFirst({ + where: { url }, + }); + } + async getFileStats(userId) { + const files = await this.prisma.file.findMany({ + where: { userId }, + select: { + type: true, + size: true, + }, + }); + const totalFiles = files.length; + const totalSize = files.reduce((sum, file) => sum + file.size, 0); + const filesByType = files.reduce((acc, file) => { + const type = file.type.split("/")[0]; // Получаем основной тип (image, application, etc.) + acc[type] = (acc[type] || 0) + 1; + return acc; + }, {}); + return { + totalFiles, + totalSize, + filesByType, + }; + } + /** + * Находит файлы старше указанной даты + */ + async findOlderThan(date) { + return this.prisma.file.findMany({ + where: { + createdAt: { + lt: date, + }, + }, + }); + } + /** + * Находит файл по названию и размеру + */ + async findByNameAndSize(name, size) { + return this.prisma.file.findFirst({ + where: { + name, + size, + }, + orderBy: { + createdAt: "desc", + }, + }); + } + /** + * Находит файл по оригинальному названию и размеру + * Используется для поиска конвертированных файлов по их оригинальным данным + */ + async findByOriginalNameAndSize(originalName, originalSize) { + return this.prisma.file.findFirst({ + where: { + originalName, + originalSize, + converted: true, + }, + orderBy: { + createdAt: "desc", + }, + }); + } + async findByNameAndSizeOrOriginal(name, size) { + const byCurrentData = await this.findByNameAndSize(name, size); + if (byCurrentData) { + return byCurrentData; + } + return await this.findByOriginalNameAndSize(name, size); + } +} +exports.FileRepository = FileRepository; diff --git a/GPTutor-Backend-v2/dist/repositories/UsageRepository.d.ts b/GPTutor-Backend-v2/dist/repositories/UsageRepository.d.ts new file mode 100644 index 00000000..537a7a4b --- /dev/null +++ b/GPTutor-Backend-v2/dist/repositories/UsageRepository.d.ts @@ -0,0 +1,60 @@ +import { PrismaClient, UsageRecord } from "@prisma/client"; +export interface CreateUsageRecordData { + userId: string; + model: string; + promptTokens: number; + completionTokens: number; + totalTokens: number; + costRub: number; + costUsd?: number; + duration: number; + stream?: boolean; + requestId?: string; + messagesCount?: number; + temperature?: number; + maxTokens?: number; + topP?: number; + frequencyPenalty?: number; + presencePenalty?: number; +} +export interface UsageRecordFilters { + userId?: string; + model?: string; + startDate?: Date; + endDate?: Date; + stream?: boolean; +} +export interface PaginationOptions { + page: number; + limit: number; + sortBy?: 'createdAt' | 'costRub' | 'totalTokens'; + sortOrder?: 'asc' | 'desc'; +} +export interface PaginatedUsageRecords { + records: UsageRecord[]; + total: number; + page: number; + limit: number; + totalPages: number; +} +export declare class UsageRepository { + private prisma; + constructor(prisma: PrismaClient); + create(data: CreateUsageRecordData): Promise; + findPaginated(filters: UsageRecordFilters | undefined, pagination: PaginationOptions): Promise; + getUsageStats(userId?: string, startDate?: Date, endDate?: Date): Promise<{ + totalRequests: number; + totalTokens: number; + totalCostRub: number; + totalCostUsd: number; + averageDuration: number; + modelsUsed: Array<{ + model: string; + requests: number; + tokens: number; + costRub: number; + }>; + }>; + findByRequestId(requestId: string): Promise; + deleteOldRecords(olderThan: Date): Promise; +} diff --git a/GPTutor-Backend-v2/dist/repositories/UsageRepository.js b/GPTutor-Backend-v2/dist/repositories/UsageRepository.js new file mode 100644 index 00000000..40d3684e --- /dev/null +++ b/GPTutor-Backend-v2/dist/repositories/UsageRepository.js @@ -0,0 +1,155 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.UsageRepository = void 0; +class UsageRepository { + constructor(prisma) { + this.prisma = prisma; + } + async create(data) { + return this.prisma.usageRecord.create({ + data: { + userId: data.userId, + model: data.model, + promptTokens: data.promptTokens, + completionTokens: data.completionTokens, + totalTokens: data.totalTokens, + costRub: data.costRub, + costUsd: data.costUsd, + duration: data.duration, + stream: data.stream ?? false, + requestId: data.requestId, + messagesCount: data.messagesCount, + temperature: data.temperature, + maxTokens: data.maxTokens, + topP: data.topP, + frequencyPenalty: data.frequencyPenalty, + presencePenalty: data.presencePenalty, + }, + }); + } + async findPaginated(filters = {}, pagination) { + const { page, limit, sortBy = 'createdAt', sortOrder = 'desc' } = pagination; + const skip = (page - 1) * limit; + const where = {}; + if (filters.userId) { + where.userId = filters.userId; + } + if (filters.model) { + where.model = filters.model; + } + if (filters.stream !== undefined) { + where.stream = filters.stream; + } + if (filters.startDate || filters.endDate) { + where.createdAt = {}; + if (filters.startDate) { + where.createdAt.gte = filters.startDate; + } + if (filters.endDate) { + where.createdAt.lte = filters.endDate; + } + } + const [records, total] = await Promise.all([ + this.prisma.usageRecord.findMany({ + where, + skip, + take: limit, + orderBy: { + [sortBy]: sortOrder, + }, + include: { + user: { + select: { + id: true, + username: true, + email: true, + vkId: true, + }, + }, + }, + }), + this.prisma.usageRecord.count({ where }), + ]); + const totalPages = Math.ceil(total / limit); + return { + records, + total, + page, + limit, + totalPages, + }; + } + async getUsageStats(userId, startDate, endDate) { + const where = {}; + if (userId) { + where.userId = userId; + } + if (startDate || endDate) { + where.createdAt = {}; + if (startDate) { + where.createdAt.gte = startDate; + } + if (endDate) { + where.createdAt.lte = endDate; + } + } + const [totalRequests, totalTokens, totalCostRub, totalCostUsd, averageDuration, modelStats,] = await Promise.all([ + this.prisma.usageRecord.count({ where }), + this.prisma.usageRecord.aggregate({ + where, + _sum: { totalTokens: true }, + }), + this.prisma.usageRecord.aggregate({ + where, + _sum: { costRub: true }, + }), + this.prisma.usageRecord.aggregate({ + where, + _sum: { costUsd: true }, + }), + this.prisma.usageRecord.aggregate({ + where, + _avg: { duration: true }, + }), + this.prisma.usageRecord.groupBy({ + by: ['model'], + where, + _count: { model: true }, + _sum: { + totalTokens: true, + costRub: true, + }, + }), + ]); + const modelsUsed = modelStats.map((stat) => ({ + model: stat.model, + requests: stat._count.model, + tokens: stat._sum.totalTokens || 0, + costRub: stat._sum.costRub || 0, + })); + return { + totalRequests, + totalTokens: totalTokens._sum.totalTokens || 0, + totalCostRub: totalCostRub._sum.costRub || 0, + totalCostUsd: totalCostUsd._sum.costUsd || 0, + averageDuration: Math.round(averageDuration._avg.duration || 0), + modelsUsed, + }; + } + async findByRequestId(requestId) { + return this.prisma.usageRecord.findFirst({ + where: { requestId }, + }); + } + async deleteOldRecords(olderThan) { + const result = await this.prisma.usageRecord.deleteMany({ + where: { + createdAt: { + lt: olderThan, + }, + }, + }); + return result.count; + } +} +exports.UsageRepository = UsageRepository; diff --git a/GPTutor-Backend-v2/dist/repositories/UserRepository.d.ts b/GPTutor-Backend-v2/dist/repositories/UserRepository.d.ts new file mode 100644 index 00000000..05415650 --- /dev/null +++ b/GPTutor-Backend-v2/dist/repositories/UserRepository.d.ts @@ -0,0 +1,16 @@ +import { PrismaClient, User } from "@prisma/client"; +export declare class UserRepository { + private prisma; + constructor(prisma: PrismaClient); + private generateApiKey; + findByVkId(vkId: string): Promise; + create(data: { + vkId: string; + balance?: number; + isActive?: boolean; + }): Promise; + findByApiKey(apiKey: string): Promise; + updateBalance(userId: string, newBalance: number): Promise; + decreaseBalance(userId: string, amount: number): Promise; + updateApiKey(userId: string): Promise; +} diff --git a/GPTutor-Backend-v2/dist/repositories/UserRepository.js b/GPTutor-Backend-v2/dist/repositories/UserRepository.js new file mode 100644 index 00000000..64ebe0e6 --- /dev/null +++ b/GPTutor-Backend-v2/dist/repositories/UserRepository.js @@ -0,0 +1,61 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.UserRepository = void 0; +class UserRepository { + constructor(prisma) { + this.prisma = prisma; + } + generateApiKey() { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let result = "sk-"; + for (let i = 0; i < 48; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; + } + async findByVkId(vkId) { + return this.prisma.user.findUnique({ + where: { vkId }, + }); + } + async create(data) { + return this.prisma.user.create({ + data: { + vkId: data.vkId, + balance: data.balance, + isActive: data.isActive ?? true, + apiKey: this.generateApiKey(), + }, + }); + } + async findByApiKey(apiKey) { + return this.prisma.user.findUnique({ + where: { apiKey }, + }); + } + async updateBalance(userId, newBalance) { + return this.prisma.user.update({ + where: { id: userId }, + data: { balance: newBalance }, + }); + } + async decreaseBalance(userId, amount) { + const user = await this.prisma.user.findUnique({ where: { id: userId } }); + if (!user) { + throw new Error("User not found"); + } + const newBalance = user.balance - amount; + if (newBalance < 0) { + throw new Error("Insufficient balance"); + } + return this.updateBalance(userId, newBalance); + } + async updateApiKey(userId) { + const newApiKey = this.generateApiKey(); + return this.prisma.user.update({ + where: { id: userId }, + data: { apiKey: newApiKey }, + }); + } +} +exports.UserRepository = UserRepository; diff --git a/GPTutor-Backend-v2/dist/services/AuthService.d.ts b/GPTutor-Backend-v2/dist/services/AuthService.d.ts new file mode 100644 index 00000000..d3d0216f --- /dev/null +++ b/GPTutor-Backend-v2/dist/services/AuthService.d.ts @@ -0,0 +1,14 @@ +import { VKUserData } from "../utils/vkAuth"; +import { UserRepository } from "../repositories/UserRepository"; +import { User } from "@prisma/client"; +export declare class AuthService { + private userRepository; + private vkAppId; + private vkSecretKey; + constructor(userRepository: UserRepository, vkAppId: string, vkSecretKey: string); + authorizeVKUser(token: string): Promise<{ + vkData: VKUserData; + dbUser: User; + }>; + updateUserToken(userId: string): Promise; +} diff --git a/GPTutor-Backend-v2/dist/services/AuthService.js b/GPTutor-Backend-v2/dist/services/AuthService.js new file mode 100644 index 00000000..f78d13ad --- /dev/null +++ b/GPTutor-Backend-v2/dist/services/AuthService.js @@ -0,0 +1,37 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.AuthService = void 0; +const vkAuth_1 = require("../utils/vkAuth"); +class AuthService { + constructor(userRepository, vkAppId, vkSecretKey) { + this.userRepository = userRepository; + this.vkAppId = vkAppId; + this.vkSecretKey = vkSecretKey; + } + async authorizeVKUser(token) { + if (!(0, vkAuth_1.validateVKSignature)(token, this.vkSecretKey)) { + throw new Error("Invalid VK signature"); + } + const userData = (0, vkAuth_1.extractVKUserData)(token); + if (!userData || !userData.vk_user_id) { + throw new Error("Invalid VK user data"); + } + console.log(userData.vk_app_id); + console.log(this.vkAppId); + if (userData.vk_app_id !== this.vkAppId) { + throw new Error("Invalid VK app ID"); + } + let user = await this.userRepository.findByVkId(userData.vk_user_id); + if (!user) { + user = await this.userRepository.create({ vkId: userData.vk_user_id }); + console.log(`Created new user with VK ID: ${userData.vk_user_id}`); + } + return { vkData: userData, dbUser: user }; + } + async updateUserToken(userId) { + const user = await this.userRepository.updateApiKey(userId); + console.log(`Updated API key for user: ${userId}`); + return user; + } +} +exports.AuthService = AuthService; diff --git a/GPTutor-Backend-v2/dist/services/FileCleanupService.d.ts b/GPTutor-Backend-v2/dist/services/FileCleanupService.d.ts new file mode 100644 index 00000000..cd478d68 --- /dev/null +++ b/GPTutor-Backend-v2/dist/services/FileCleanupService.d.ts @@ -0,0 +1,41 @@ +import { PrismaClient } from "@prisma/client"; +import { FilesService } from "./FilesService"; +/** + * Сервис для автоматической очистки старых файлов + */ +export declare class FileCleanupService { + private prisma; + private filesService; + private cleanupInterval; + private readonly FILE_LIFETIME_HOURS; + private readonly CLEANUP_INTERVAL_MS; + constructor(prisma: PrismaClient, filesService: FilesService); + /** + * Запускает автоматическую очистку файлов + */ + start(): void; + /** + * Останавливает автоматическую очистку файлов + */ + stop(): void; + /** + * Выполняет очистку старых файлов + */ + private cleanupOldFiles; + /** + * Вычисляет возраст файла в часах + */ + private getFileAge; + /** + * Ручная очистка старых файлов (для вызова из API или CLI) + */ + cleanupNow(): Promise<{ + success: boolean; + message: string; + stats?: { + total: number; + success: number; + errors: number; + }; + }>; +} diff --git a/GPTutor-Backend-v2/dist/services/FileCleanupService.js b/GPTutor-Backend-v2/dist/services/FileCleanupService.js new file mode 100644 index 00000000..461ec367 --- /dev/null +++ b/GPTutor-Backend-v2/dist/services/FileCleanupService.js @@ -0,0 +1,131 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.FileCleanupService = void 0; +const LoggerService_1 = require("./LoggerService"); +/** + * Сервис для автоматической очистки старых файлов + */ +class FileCleanupService { + constructor(prisma, filesService) { + this.cleanupInterval = null; + this.FILE_LIFETIME_HOURS = 20; + this.CLEANUP_INTERVAL_MS = 60 * 60 * 1000; + this.prisma = prisma; + this.filesService = filesService; + } + /** + * Запускает автоматическую очистку файлов + */ + start() { + LoggerService_1.logger.info("Starting file cleanup service", { + lifetimeHours: this.FILE_LIFETIME_HOURS, + checkIntervalMinutes: this.CLEANUP_INTERVAL_MS / (60 * 1000), + }); + this.cleanupOldFiles(); + this.cleanupInterval = setInterval(() => { + this.cleanupOldFiles(); + }, this.CLEANUP_INTERVAL_MS); + } + /** + * Останавливает автоматическую очистку файлов + */ + stop() { + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval); + this.cleanupInterval = null; + LoggerService_1.logger.info("File cleanup service stopped"); + } + } + /** + * Выполняет очистку старых файлов + */ + async cleanupOldFiles() { + try { + const cutoffDate = new Date(); + cutoffDate.setHours(cutoffDate.getHours() - this.FILE_LIFETIME_HOURS); + LoggerService_1.logger.info("Starting file cleanup", { + cutoffDate: cutoffDate.toISOString(), + lifetimeHours: this.FILE_LIFETIME_HOURS, + }); + const oldFiles = await this.prisma.file.findMany({ + where: { + createdAt: { + lt: cutoffDate, + }, + }, + }); + if (oldFiles.length === 0) { + LoggerService_1.logger.info("No old files to cleanup"); + return; + } + LoggerService_1.logger.info(`Found ${oldFiles.length} old files to cleanup`, { + fileIds: oldFiles.map((f) => f.id), + }); + let successCount = 0; + let errorCount = 0; + // Удаляем каждый файл + for (const file of oldFiles) { + try { + // Удаляем только запись из БД (файл остается в S3) + await this.prisma.file.delete({ + where: { id: file.id }, + }); + successCount++; + LoggerService_1.logger.info("File cleaned up successfully", { + fileId: file.id, + fileName: file.name, + userId: file.userId, + age: this.getFileAge(file.createdAt), + }); + } + catch (error) { + errorCount++; + LoggerService_1.logger.error("Failed to cleanup file", error, { + fileId: file.id, + fileName: file.name, + url: file.url, + }); + } + } + LoggerService_1.logger.info("File cleanup completed", { + totalFiles: oldFiles.length, + successCount, + errorCount, + cutoffDate: cutoffDate.toISOString(), + }); + } + catch (error) { + LoggerService_1.logger.error("File cleanup failed", error); + } + } + /** + * Вычисляет возраст файла в часах + */ + getFileAge(createdAt) { + const now = new Date(); + const ageMs = now.getTime() - createdAt.getTime(); + const ageHours = Math.floor(ageMs / (1000 * 60 * 60)); + const ageMinutes = Math.floor((ageMs % (1000 * 60 * 60)) / (1000 * 60)); + return `${ageHours}h ${ageMinutes}m`; + } + /** + * Ручная очистка старых файлов (для вызова из API или CLI) + */ + async cleanupNow() { + try { + await this.cleanupOldFiles(); + return { + success: true, + message: "Cleanup completed successfully", + }; + } + catch (error) { + LoggerService_1.logger.error("Manual cleanup failed", error); + return { + success: false, + message: "Cleanup failed", + }; + } + } +} +exports.FileCleanupService = FileCleanupService; diff --git a/GPTutor-Backend-v2/dist/services/FilesService.d.ts b/GPTutor-Backend-v2/dist/services/FilesService.d.ts new file mode 100644 index 00000000..77e94690 --- /dev/null +++ b/GPTutor-Backend-v2/dist/services/FilesService.d.ts @@ -0,0 +1,27 @@ +import { S3 } from "aws-sdk"; +export declare class FilesService { + private getExtension; + private getFileWithExtension; + /** + * Проверяет, нужна ли конвертация файла в PDF + */ + private needsConversionToPdf; + /** + * Конвертирует документ в PDF с помощью LibreOffice (через libreoffice-convert) + * Работает напрямую с буферами без создания временных файлов + */ + private convertToPdf; + private optimizePhotos; + private optimizeAttachment; + private determineFileType; + uploadFile(arrayBuffer: ArrayBuffer | string | Buffer | Uint8Array, name: string): Promise; + optimizeAndUploadFile(arrayBuffer: ArrayBuffer, fileName: string): Promise<{ + url: string; + optimizedData: ArrayBuffer | Buffer | string | Uint8Array; + finalFileName: string; + }>; + /** + * Удаляет файл из S3 по URL + */ + deleteFile(fileUrl: string): Promise; +} diff --git a/GPTutor-Backend-v2/dist/services/FilesService.js b/GPTutor-Backend-v2/dist/services/FilesService.js new file mode 100644 index 00000000..72538f01 --- /dev/null +++ b/GPTutor-Backend-v2/dist/services/FilesService.js @@ -0,0 +1,255 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.FilesService = void 0; +const sharp_1 = __importDefault(require("sharp")); +const compress_pdf_1 = require("compress-pdf"); +//@ts-ignore +const easy_yandex_s3_1 = __importDefault(require("easy-yandex-s3")); +const crypto_1 = __importDefault(require("crypto")); +const LoggerService_1 = require("./LoggerService"); +//@ts-ignore +const libreoffice_convert_1 = __importDefault(require("libreoffice-convert")); +const util_1 = require("util"); +// Используем extend для callback, чтобы избежать deprecation warning +const libreConvert = (0, util_1.promisify)(libreoffice_convert_1.default.convert.bind(libreoffice_convert_1.default)); +const s3 = new easy_yandex_s3_1.default({ + auth: { + accessKeyId: process.env.YANDEX_ACCESS_KEY_ID, + secretAccessKey: process.env.YANDEX_SECRET_ACCESS_KEY, + }, + httpOptions: { + timeout: 60000, + }, + Bucket: process.env.YANDEX_BUCKET, + debug: process.env.NODE_ENV === "development", +}); +class FilesService { + getExtension(fileName) { + const splitFileName = fileName.split("."); + return splitFileName[splitFileName.length - 1].toLowerCase(); + } + getFileWithExtension(name, originalFileName) { + return `${name}.${this.getExtension(originalFileName)}`; + } + /** + * Проверяет, нужна ли конвертация файла в PDF + */ + needsConversionToPdf(fileName) { + const extension = this.getExtension(fileName); + const convertibleExtensions = ["doc", "docx", "ppt", "pptx"]; + return convertibleExtensions.includes(extension); + } + /** + * Конвертирует документ в PDF с помощью LibreOffice (через libreoffice-convert) + * Работает напрямую с буферами без создания временных файлов + */ + async convertToPdf(buffer, originalFileName) { + const startTime = Date.now(); + try { + LoggerService_1.logger.info("Converting document to PDF using libreoffice-convert", { + fileName: originalFileName, + fileSize: buffer.length, + }); + // Определяем расширение выходного файла + const ext = ".pdf"; + // Конвертируем буфер напрямую в PDF + // @ts-ignore - libreoffice-convert не имеет типов TypeScript + const pdfBuffer = await libreConvert(buffer, ext, undefined); + const duration = Date.now() - startTime; + // Проверяем результат + if (!pdfBuffer || pdfBuffer.length === 0) { + throw new Error("Conversion resulted in empty buffer"); + } + // Формируем финальное имя файла + const baseFileName = originalFileName.substring(0, originalFileName.lastIndexOf(".")); + const finalPdfFileName = `${baseFileName}.pdf`; + LoggerService_1.logger.info("Document converted to PDF successfully", { + originalFileName, + finalPdfFileName, + originalSize: buffer.length, + pdfSize: pdfBuffer.length, + durationMs: duration, + }); + return { + buffer: pdfBuffer, + newFileName: finalPdfFileName, + }; + } + catch (error) { + const duration = Date.now() - startTime; + const errorMessage = error.message; + LoggerService_1.logger.error("Failed to convert document to PDF", error, { + fileName: originalFileName, + durationMs: duration, + errorMessage, + }); + // Определяем тип ошибки для более понятного сообщения + if (errorMessage.includes("Could not find platform independent libraries") || + errorMessage.includes("soffice") || + errorMessage.includes("LibreOffice")) { + throw new Error("LibreOffice not found. Please install LibreOffice on your system. " + + "Visit: https://www.libreoffice.org/download/"); + } + if (errorMessage.includes("Document is empty")) { + throw new Error("Failed to read document. The file may be corrupted or in an unsupported format."); + } + throw new Error(`Failed to convert document to PDF: ${errorMessage}`); + } + } + async optimizePhotos(arrayBuffer, fileName) { + try { + let extension = this.getExtension(fileName); + if (extension === "jpg") { + extension = "jpeg"; + } + const createdSharp = (0, sharp_1.default)(arrayBuffer); + if (extension in createdSharp) { + // @ts-ignore + return await createdSharp[extension]({ quality: 60 }).toBuffer(); + } + return Buffer.from(arrayBuffer); + } + catch (error) { + console.log("Error optimizing photo:", error); + return Buffer.from(arrayBuffer); + } + } + async optimizeAttachment(arrayBuffer, fileName) { + const typeFile = this.determineFileType(fileName); + const extension = this.getExtension(fileName); + LoggerService_1.logger.info("TypeFile", typeFile); + // Конвертируем doc/docx/ppt/pptx в PDF + if (this.needsConversionToPdf(fileName)) { + LoggerService_1.logger.info("Document needs conversion to PDF", { fileName, extension }); + const buffer = Buffer.from(arrayBuffer); + const { buffer: pdfBuffer } = await this.convertToPdf(buffer, fileName); + // Оптимизируем полученный PDF + LoggerService_1.logger.info("Optimizing converted PDF"); + return await (0, compress_pdf_1.compress)(pdfBuffer); + } + if (typeFile === "photo") { + return await this.optimizePhotos(arrayBuffer, fileName); + } + if (typeFile === "text") { + return Buffer.from(arrayBuffer).toString("utf-8"); + } + if (extension === "pdf") { + return await (0, compress_pdf_1.compress)(Buffer.from(arrayBuffer)); + } + return Buffer.from(arrayBuffer); + } + determineFileType(filename) { + if (filename.length === 0) { + throw new Error("Invalid filename: Must be a non-empty string."); + } + const photoExtensions = [ + "jpg", + "jpeg", + "png", + "gif", + "bmp", + "svg", + "webp", + "tiff", + "tif", + ]; + const documentExtensions = [ + "pdf", + "doc", + "docx", + "xls", + "xlsx", + "csv", + "ppt", + "pptx", + ]; + const textExtensions = [ + "txt", + "js", + "html", + "css", + "json", + "xml", + "md", + "log", + "py", + "java", + "c", + "cpp", + "h", + "sh", + "config", + "conf", + "ini", + "yml", + "yaml", + "sql", + ]; + const dotIndex = filename.lastIndexOf("."); + if (dotIndex === -1 || dotIndex === filename.length - 1) { + throw new Error(`Unknown file type: '${filename}' has no extension.`); + } + const extension = filename.slice(dotIndex + 1).toLowerCase(); + if (photoExtensions.includes(extension)) { + return "photo"; + } + else if (documentExtensions.includes(extension)) { + return "document"; + } + else if (textExtensions.includes(extension)) { + return "text"; + } + else { + throw new Error(`Unknown or unsupported file type with extension '.${extension}'.`); + } + } + async uploadFile(arrayBuffer, name) { + console.log(arrayBuffer); + LoggerService_1.logger.info("File Name", { + name: this.getFileWithExtension(crypto_1.default.randomUUID(), name), + }); + return (await s3.Upload({ + //@ts-ignore + buffer: arrayBuffer, + name: this.getFileWithExtension(crypto_1.default.randomUUID(), name), + }, "/")); + } + async optimizeAndUploadFile(arrayBuffer, fileName) { + let finalFileName = fileName; + console.log("this.needsConversionToPdf(fileName)", this.needsConversionToPdf(fileName)); + if (this.needsConversionToPdf(fileName)) { + const baseFileName = fileName.substring(0, fileName.lastIndexOf(".")); + finalFileName = `${baseFileName}.pdf`; + } + const optimizedData = await this.optimizeAttachment(arrayBuffer, fileName); + const uploadResult = await this.uploadFile(optimizedData, finalFileName); + console.log(uploadResult); + LoggerService_1.logger.info("UploadResult", uploadResult); + return { + url: uploadResult.Location, + optimizedData, + finalFileName, + }; + } + /** + * Удаляет файл из S3 по URL + */ + async deleteFile(fileUrl) { + try { + // Извлекаем имя файла из URL + const url = new URL(fileUrl); + const fileName = url.pathname.substring(1); // Убираем первый слеш + LoggerService_1.logger.info("Deleting file from S3", { fileName, fileUrl }); + await s3.Remove(fileName); + LoggerService_1.logger.info("File deleted from S3 successfully", { fileName }); + } + catch (error) { + LoggerService_1.logger.error("Failed to delete file from S3", error, { fileUrl }); + throw error; + } + } +} +exports.FilesService = FilesService; diff --git a/GPTutor-Backend-v2/dist/services/LLMCostEvaluate.d.ts b/GPTutor-Backend-v2/dist/services/LLMCostEvaluate.d.ts new file mode 100644 index 00000000..5340add0 --- /dev/null +++ b/GPTutor-Backend-v2/dist/services/LLMCostEvaluate.d.ts @@ -0,0 +1,13 @@ +import { OpenRouterModel } from "../types/openrouter"; +export declare class LLMCostEvaluate { + private models; + private usdToRubRate; + private readonly OPENROUTER_API_URL; + constructor(usdToRubRate?: number); + initialize(): Promise; + getAllModels(): OpenRouterModel[]; + calculateCost(cost: number): number; + getModelsWithRubPricing(searchTerm?: string, provider?: string): any[]; + getModelsByProviders(providers: string[]): any[]; + getPopularProviderModels(): any[]; +} diff --git a/GPTutor-Backend-v2/dist/services/LLMCostEvaluate.js b/GPTutor-Backend-v2/dist/services/LLMCostEvaluate.js new file mode 100644 index 00000000..755c4405 --- /dev/null +++ b/GPTutor-Backend-v2/dist/services/LLMCostEvaluate.js @@ -0,0 +1,78 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.LLMCostEvaluate = void 0; +class LLMCostEvaluate { + constructor(usdToRubRate = 100) { + this.models = []; + this.OPENROUTER_API_URL = "https://openrouter.ai/api/v1/models"; + this.usdToRubRate = usdToRubRate; + } + async initialize() { + try { + console.log("🔄 Loading OpenRouter Models..."); + const response = await fetch(this.OPENROUTER_API_URL); + if (!response.ok) { + throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`); + } + const data = (await response.json()); + this.models = data.data; + console.log(`✅ Loaded ${this.models.length} OpenRouter models`); + } + catch (error) { + console.error("❌ Failed to load OpenRouter Models:", error); + throw error; + } + } + getAllModels() { + return this.models; + } + calculateCost(cost) { + return cost * this.usdToRubRate * 2; + } + getModelsWithRubPricing(searchTerm, provider) { + let filteredModels = this.models; + if (searchTerm) { + const term = searchTerm.toLowerCase(); + filteredModels = filteredModels.filter((model) => model.name.toLowerCase().includes(term) || + model.id.toLowerCase().includes(term) || + model.description.toLowerCase().includes(term)); + } + if (provider) { + filteredModels = filteredModels.filter((model) => model.id.toLowerCase().includes(provider.toLowerCase())); + } + return filteredModels.map((model) => ({ + ...model, + pricing: { + prompt: this.calculateCost(parseFloat(model.pricing.prompt)), + completion: this.calculateCost(parseFloat(model.pricing.completion)), + request: this.calculateCost(parseFloat(model.pricing.request)), + image: this.calculateCost(parseFloat(model.pricing.image)), + web_search: this.calculateCost(parseFloat(model.pricing.web_search)), + internal_reasoning: this.calculateCost(parseFloat(model.pricing.internal_reasoning)), + input_cache_read: model.pricing.input_cache_read + ? this.calculateCost(parseFloat(model.pricing.input_cache_read)) + : undefined, + input_cache_write: model.pricing.input_cache_write + ? this.calculateCost(parseFloat(model.pricing.input_cache_write)) + : undefined, + }, + })); + } + getModelsByProviders(providers) { + return this.getModelsWithRubPricing().filter((model) => providers.some((provider) => model.id.toLowerCase().startsWith(`${provider.toLowerCase()}/`))); + } + getPopularProviderModels() { + const popularProviders = [ + "x-ai", + "deepseek", + "google", + "qwen", + "perplexity", + "mistralai", + "openai", + "anthropic", + ]; + return this.getModelsByProviders(popularProviders).filter((model) => !model.id.toLowerCase().includes(":free")); + } +} +exports.LLMCostEvaluate = LLMCostEvaluate; diff --git a/GPTutor-Backend-v2/dist/services/LoggerService.d.ts b/GPTutor-Backend-v2/dist/services/LoggerService.d.ts new file mode 100644 index 00000000..97f17c38 --- /dev/null +++ b/GPTutor-Backend-v2/dist/services/LoggerService.d.ts @@ -0,0 +1,22 @@ +import winston from "winston"; +export declare class LoggerService { + private logger; + private static instance; + private static safeStringify; + private constructor(); + static getInstance(): LoggerService; + private createLogger; + debug(message: string, meta?: any): void; + info(message: string, meta?: any): void; + warn(message: string, meta?: any): void; + error(message: string, error?: Error | any, meta?: any): void; + apiRequest(method: string, url: string, userId?: string, meta?: any): void; + apiResponse(method: string, url: string, statusCode: number, duration: number, userId?: string, meta?: any): void; + llmRequest(model: string, userId: string, tokens?: number, cost?: number, meta?: any): void; + llmResponse(model: string, userId: string, tokens: number, cost: number, duration: number, meta?: any): void; + auth(action: string, userId?: string, success?: boolean, meta?: any): void; + balance(action: string, userId: string, amount: number, newBalance: number, meta?: any): void; + child(defaultMeta: any): winston.Logger; + getWinstonLogger(): winston.Logger; +} +export declare const logger: LoggerService; diff --git a/GPTutor-Backend-v2/dist/services/LoggerService.js b/GPTutor-Backend-v2/dist/services/LoggerService.js new file mode 100644 index 00000000..ab64ce3d --- /dev/null +++ b/GPTutor-Backend-v2/dist/services/LoggerService.js @@ -0,0 +1,189 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.logger = exports.LoggerService = void 0; +const winston_1 = __importDefault(require("winston")); +const winston_daily_rotate_file_1 = __importDefault(require("winston-daily-rotate-file")); +const path_1 = __importDefault(require("path")); +class LoggerService { + // Safe JSON stringify that handles circular references + static safeStringify(obj, space) { + const seen = new WeakSet(); + return JSON.stringify(obj, (key, val) => { + if (val != null && typeof val === "object") { + if (seen.has(val)) { + return "[Circular]"; + } + seen.add(val); + } + return val; + }, space); + } + constructor() { + this.logger = this.createLogger(); + } + static getInstance() { + if (!LoggerService.instance) { + LoggerService.instance = new LoggerService(); + } + return LoggerService.instance; + } + createLogger() { + const logDir = path_1.default.join(process.cwd(), "logs"); + // Custom format for better readability + const customFormat = winston_1.default.format.combine(winston_1.default.format.timestamp({ + format: "YYYY-MM-DD HH:mm:ss", + }), winston_1.default.format.errors({ stack: true }), winston_1.default.format.printf(({ timestamp, level, message, service, userId, requestId, ...meta }) => { + let logMessage = `[${timestamp}] ${level.toUpperCase()}`; + if (service) + logMessage += ` [${service}]`; + if (requestId) + logMessage += ` [ReqID: ${requestId}]`; + if (userId) + logMessage += ` [User: ${userId}]`; + logMessage += `: ${message}`; + const metaStr = Object.keys(meta).length + ? ` ${LoggerService.safeStringify(meta)}` + : ""; + return logMessage + metaStr; + })); + const consoleFormat = winston_1.default.format.combine(winston_1.default.format.colorize(), customFormat); + return winston_1.default.createLogger({ + level: process.env.LOG_LEVEL || "info", + format: customFormat, + defaultMeta: { service: "gptutor-backend" }, + transports: [ + new winston_1.default.transports.Console({ + format: consoleFormat, + }), + new winston_daily_rotate_file_1.default({ + filename: path_1.default.join(logDir, "error-%DATE%.log"), + datePattern: "YYYY-MM-DD", + level: "error", + maxSize: "20m", + maxFiles: "14d", + format: winston_1.default.format.combine(winston_1.default.format.timestamp(), winston_1.default.format.json()), + }), + new winston_daily_rotate_file_1.default({ + filename: path_1.default.join(logDir, "combined-%DATE%.log"), + datePattern: "YYYY-MM-DD", + maxSize: "20m", + maxFiles: "30d", + format: winston_1.default.format.combine(winston_1.default.format.timestamp(), winston_1.default.format.json()), + }), + // API requests log file + new winston_daily_rotate_file_1.default({ + filename: path_1.default.join(logDir, "api-%DATE%.log"), + datePattern: "YYYY-MM-DD", + maxSize: "20m", + maxFiles: "30d", + level: "info", + format: winston_1.default.format.combine(winston_1.default.format.timestamp(), winston_1.default.format.json()), + }), + ], + }); + } + // Basic logging methods + debug(message, meta) { + this.logger.debug(message, meta); + } + info(message, meta) { + this.logger.info(message, meta); + } + warn(message, meta) { + this.logger.warn(message, meta); + } + error(message, error, meta) { + if (error instanceof Error) { + this.logger.error(message, { + error: error.message, + stack: error.stack, + ...meta, + }); + } + else { + this.logger.error(message, { error, ...meta }); + } + } + // Specialized logging methods + apiRequest(method, url, userId, meta) { + this.logger.info(`${method} ${url}`, { + type: "api", + service: "api", + method, + url, + userId, + ...meta, + }); + } + apiResponse(method, url, statusCode, duration, userId, meta) { + this.logger.info(`${method} ${url} - ${statusCode} (${duration}ms)`, { + type: "api", + service: "api", + method, + url, + statusCode, + duration, + userId, + ...meta, + }); + } + llmRequest(model, userId, tokens, cost, meta) { + this.logger.info(`LLM Request: ${model}`, { + type: "llm", + service: "llm", + model, + userId, + tokens, + cost, + ...meta, + }); + } + llmResponse(model, userId, tokens, cost, duration, meta) { + this.logger.info(`LLM Response: ${model} - ${tokens} tokens, ${cost} RUB (${duration}ms)`, { + type: "llm", + service: "llm", + model, + userId, + tokens, + cost, + duration, + ...meta, + }); + } + auth(action, userId, success = true, meta) { + const level = success ? "info" : "warn"; + this.logger[level](`Auth ${action}: ${success ? "SUCCESS" : "FAILED"}`, { + type: "auth", + service: "auth", + action, + userId, + success, + ...meta, + }); + } + balance(action, userId, amount, newBalance, meta) { + this.logger.info(`Balance ${action}: ${amount} RUB`, { + type: "balance", + service: "balance", + action, + userId, + amount, + newBalance, + ...meta, + }); + } + // Create child logger with additional context + child(defaultMeta) { + return this.logger.child(defaultMeta); + } + // Get the underlying winston logger + getWinstonLogger() { + return this.logger; + } +} +exports.LoggerService = LoggerService; +// Export singleton instance +exports.logger = LoggerService.getInstance(); diff --git a/GPTutor-Backend-v2/dist/services/OpenRouterService.d.ts b/GPTutor-Backend-v2/dist/services/OpenRouterService.d.ts new file mode 100644 index 00000000..36732a2d --- /dev/null +++ b/GPTutor-Backend-v2/dist/services/OpenRouterService.d.ts @@ -0,0 +1,11 @@ +import OpenAI from "openai"; +import type { ChatCompletion, ChatCompletionCreateParams } from "openai/resources/chat/completions"; +export declare class OpenRouterService { + private client; + constructor(apiKey: string); + createCompletion(params: ChatCompletionCreateParams): Promise; + createCompletionStream(params: ChatCompletionCreateParams): Promise>; + getModels(): Promise; +} diff --git a/GPTutor-Backend-v2/dist/services/OpenRouterService.js b/GPTutor-Backend-v2/dist/services/OpenRouterService.js new file mode 100644 index 00000000..8f1becaf --- /dev/null +++ b/GPTutor-Backend-v2/dist/services/OpenRouterService.js @@ -0,0 +1,60 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.OpenRouterService = void 0; +const openai_1 = __importDefault(require("openai")); +class OpenRouterService { + constructor(apiKey) { + this.client = new openai_1.default({ + apiKey: apiKey, + baseURL: "https://openrouter.ai/api/v1", + defaultHeaders: { + "HTTP-Referer": "http://localhost:3001", + "X-Title": "GPTutor API v2", + }, + }); + } + async createCompletion(params) { + try { + const completion = await this.client.chat.completions.create({ + ...params, + stream: false, + usage: { include: true }, + }); + return completion; + } + catch (error) { + console.error("OpenRouter API request failed:", error); + throw error; + } + } + async createCompletionStream(params) { + try { + //@ts-ignore + return this.client.chat.completions.create({ + ...params, + stream: true, + usage: { + include: true, + }, + }); + } + catch (error) { + console.error("OpenRouter API stream request failed:", error); + throw error; + } + } + async getModels() { + try { + const models = await this.client.models.list(); + return models; + } + catch (error) { + console.error("Failed to get Models from OpenRouter:", error); + throw error; + } + } +} +exports.OpenRouterService = OpenRouterService; diff --git a/GPTutor-Backend-v2/dist/types/openrouter.d.ts b/GPTutor-Backend-v2/dist/types/openrouter.d.ts new file mode 100644 index 00000000..f0f3814c --- /dev/null +++ b/GPTutor-Backend-v2/dist/types/openrouter.d.ts @@ -0,0 +1,58 @@ +export interface OpenRouterPricing { + prompt: string; + completion: string; + request: string; + image: string; + web_search: string; + internal_reasoning: string; + input_cache_read?: string; + input_cache_write?: string; +} +export interface OpenRouterArchitecture { + modality: string; + input_modalities: string[]; + output_modalities: string[]; + tokenizer: string; + instruct_type: string | null; +} +export interface OpenRouterTopProvider { + context_length: number; + max_completion_tokens: number; + is_moderated: boolean; +} +export interface OpenRouterDefaultParameters { + temperature: number | null; + top_p: number | null; + frequency_penalty: number | null; +} +export interface OpenRouterModel { + id: string; + canonical_slug: string; + hugging_face_id: string; + name: string; + created: number; + description: string; + context_length: number; + architecture: OpenRouterArchitecture; + pricing: OpenRouterPricing; + top_provider: OpenRouterTopProvider; + per_request_limits: any; + supported_parameters: string[]; + default_parameters: OpenRouterDefaultParameters; +} +export interface OpenRouterApiResponse { + data: OpenRouterModel[]; +} +export interface CostCalculation { + promptCostRub: number; + completionCostRub: number; + requestCostRub: number; + imageCostRub: number; + totalCostRub: number; +} +export interface UsageParams { + promptTokens?: number; + completionTokens?: number; + images?: number; + requests?: number; +} diff --git a/GPTutor-Backend-v2/dist/types/openrouter.js b/GPTutor-Backend-v2/dist/types/openrouter.js new file mode 100644 index 00000000..c8ad2e54 --- /dev/null +++ b/GPTutor-Backend-v2/dist/types/openrouter.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/GPTutor-Backend-v2/dist/utils/vkAuth.d.ts b/GPTutor-Backend-v2/dist/utils/vkAuth.d.ts new file mode 100644 index 00000000..71a718ed --- /dev/null +++ b/GPTutor-Backend-v2/dist/utils/vkAuth.d.ts @@ -0,0 +1,20 @@ +import { UserRepository } from "../repositories/UserRepository"; +export declare function validateVKSignature(queryString: string, secretKey: string): boolean; +export declare function extractVKUserData(queryString: string): { + vk_user_id: string | null; + vk_app_id: string | null; + vk_is_app_user: string | null; + vk_language: string | null; + vk_platform: string | null; + vk_ts: string | null; +} | null; +export interface VKUserData { + vk_user_id: string | null; + vk_app_id: string | null; + vk_is_app_user: string | null; + vk_language: string | null; + vk_platform: string | null; + vk_ts: string | null; +} +export declare function validateApiKey(apiKey: string, userRepository: UserRepository): Promise; +export declare function authenticateUser(authHeader: string | undefined, vkSecretKey: string, userRepository: UserRepository): Promise; diff --git a/GPTutor-Backend-v2/dist/utils/vkAuth.js b/GPTutor-Backend-v2/dist/utils/vkAuth.js new file mode 100644 index 00000000..0d85f405 --- /dev/null +++ b/GPTutor-Backend-v2/dist/utils/vkAuth.js @@ -0,0 +1,140 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.validateVKSignature = validateVKSignature; +exports.extractVKUserData = extractVKUserData; +exports.validateApiKey = validateApiKey; +exports.authenticateUser = authenticateUser; +const crypto = __importStar(require("crypto")); +function validateVKSignature(queryString, secretKey) { + console.log({ queryString }); + console.log({ secretKey }); + try { + // Handle both full URL and query string + let params; + if (queryString.startsWith("http")) { + // Full URL + const url = new URL(queryString); + params = new URLSearchParams(url.search); + } + else { + // Query string only + params = new URLSearchParams(queryString); + } + const sign = params.get("sign"); + if (!sign) + return false; + params.delete("sign"); + const sortedParams = Array.from(params.entries()) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, value]) => `${key}=${value}`) + .join("&"); + const hmac = crypto.createHmac("sha256", secretKey); + hmac.update(sortedParams); + const computedSign = hmac.digest("base64url"); + // Debug logging + console.log("VK Signature validation:"); + console.log("Sorted params:", sortedParams); + console.log("Received sign:", sign); + console.log("Computed sign:", computedSign); + console.log("Signatures match:", computedSign === sign); + return computedSign === sign; + } + catch (error) { + console.error("VK signature validation error:", error); + return false; + } +} +function extractVKUserData(queryString) { + try { + // Handle both full URL and query string + let params; + if (queryString.startsWith("http")) { + // Full URL + const url = new URL(queryString); + params = new URLSearchParams(url.search); + } + else { + // Query string only + params = new URLSearchParams(queryString); + } + return { + vk_user_id: params.get("vk_user_id"), + vk_app_id: params.get("vk_app_id"), + vk_is_app_user: params.get("vk_is_app_user"), + vk_language: params.get("vk_language"), + vk_platform: params.get("vk_platform"), + vk_ts: params.get("vk_ts"), + }; + } + catch (error) { + console.error("Error extracting VK user data:", error); + return null; + } +} +async function validateApiKey(apiKey, userRepository) { + try { + if (!apiKey.startsWith("sk-")) { + return null; + } + const user = await userRepository.findByApiKey(apiKey); + if (!user || !user.isActive) { + return null; + } + return user; + } + catch (error) { + console.error("API key validation error:", error); + return null; + } +} +async function authenticateUser(authHeader, vkSecretKey, userRepository) { + if (authHeader && authHeader.startsWith("Bearer ")) { + const apiKey = authHeader.substring(7); + if (apiKey.startsWith("sk-")) { + const user = await validateApiKey(apiKey, userRepository); + if (user) { + return { user, authType: "api_key" }; + } + } + if (validateVKSignature(apiKey, vkSecretKey)) { + const vkData = extractVKUserData(apiKey); + if (vkData && vkData.vk_user_id) { + return { user: vkData, authType: "vk" }; + } + } + } + return null; +} diff --git a/GPTutor-Backend-v2/docs/.dockerignore b/GPTutor-Backend-v2/docs/.dockerignore new file mode 100644 index 00000000..f2504e38 --- /dev/null +++ b/GPTutor-Backend-v2/docs/.dockerignore @@ -0,0 +1,8 @@ +node_modules +npm-debug.log +.git +.gitignore +package.json +package-lock.json +serve-internal.js + diff --git a/GPTutor-Backend-v2/docs/Dockerfile b/GPTutor-Backend-v2/docs/Dockerfile new file mode 100644 index 00000000..32fde91f --- /dev/null +++ b/GPTutor-Backend-v2/docs/Dockerfile @@ -0,0 +1,21 @@ +# Lightweight Node.js image for serving documentation +FROM node:20-alpine + +WORKDIR /app + +# Copy all documentation files +COPY . . + +# Install minimal dependencies if needed (though serve.js uses only built-in modules) +# No need to run npm install as serve.js uses only Node.js built-in modules + +# Expose port +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --quiet --tries=1 --spider http://localhost:8080/ || exit 1 + +# Start documentation server +CMD ["node", "serve.js"] + diff --git a/GPTutor-Backend-v2/docs/MODELS_API.md b/GPTutor-Backend-v2/docs/MODELS_API.md new file mode 100644 index 00000000..bb9600e7 --- /dev/null +++ b/GPTutor-Backend-v2/docs/MODELS_API.md @@ -0,0 +1,99 @@ +# Models API Documentation + +API для получения информации о моделях LLM с ценами в рублях. + +## Endpoints + +### GET /v1/models + +Получить модели только от популярных провайдеров с ценами в рублях. **Публичный endpoint - авторизация не требуется.** + +**Поддерживаемые провайдеры:** x-ai, deepseek, google, qwen, perplexity, mistralai, openai + +**Фильтрация:** Исключены бесплатные модели (содержащие ":free" в названии) + +**Сортировка:** Модели отсортированы по цене (самые дорогие сверху), затем по дате создания (самые новые сверху) + +**Ответ:** +```json +{ + "success": true, + "data": { + "models": [ + { + "id": "google/gemini-2.5-flash-lite-preview-09-2025", + "name": "Google: Gemini 2.5 Flash Lite Preview 09-2025", + "description": "Gemini 2.5 Flash-Lite is a lightweight reasoning model...", + "context_length": 1048576, + "architecture": { + "modality": "text+image->text", + "input_modalities": ["file", "image", "text", "audio"], + "output_modalities": ["text"], + "tokenizer": "Gemini", + "instruct_type": null + }, + "pricing_rub": { + "prompt": 0.000009, + "completion": 0.000036, + "request": 0, + "image": 0, + "web_search": 0, + "internal_reasoning": 0 + }, + "top_provider": { + "context_length": 1048576, + "max_completion_tokens": 65536, + "is_moderated": false + }, + "supported_parameters": [ + "include_reasoning", + "max_tokens", + "reasoning", + "response_format", + "seed", + "stop", + "structured_outputs", + "temperature", + "tool_choice", + "tools", + "top_p" + ] + } + ], + "total": 45, + "providers": ["x-ai", "deepseek", "google", "qwen", "perplexity", "mistralai", "openai"], + "currency": "RUB", + "exchangeRate": 90, + "lastUpdated": "2025-01-28T10:30:00.000Z" + } +} +``` + +## Коды ошибок + +- `400 Bad Request` - неверные параметры запроса +- `500 Internal Server Error` - внутренняя ошибка сервера + +## Примеры использования + +### Получение моделей популярных провайдеров +```bash +curl -X GET "http://localhost:3001/v1/models" +``` + +## Структура ценообразования + +Все цены возвращаются в рублях и рассчитываются по формуле: +``` +цена_в_рублях = цена_в_долларах * курс_USD_to_RUB +``` + +Текущий курс: 90 рублей за доллар (можно изменить через `setUsdToRubRate()`). + +### Типы цен: +- `prompt` - цена за входной токен +- `completion` - цена за выходной токен +- `request` - фиксированная цена за запрос +- `image` - цена за изображение +- `web_search` - цена за веб-поиск +- `internal_reasoning` - цена за внутренние рассуждения diff --git a/GPTutor-Backend-v2/docs/dark.html b/GPTutor-Backend-v2/docs/dark.html new file mode 100644 index 00000000..623136df --- /dev/null +++ b/GPTutor-Backend-v2/docs/dark.html @@ -0,0 +1,143 @@ + + + + GPTutor Backend v2 API Documentation (Dark Theme) + + + + + + + + + + + + diff --git a/GPTutor-Backend-v2/docs/index.html b/GPTutor-Backend-v2/docs/index.html new file mode 100644 index 00000000..6a01c9dc --- /dev/null +++ b/GPTutor-Backend-v2/docs/index.html @@ -0,0 +1,184 @@ + + + + GPTutor Backend v2 API Documentation + + + + + + + + + + + + diff --git a/GPTutor-Backend-v2/docs/internal-api.yaml b/GPTutor-Backend-v2/docs/internal-api.yaml new file mode 100644 index 00000000..665e315c --- /dev/null +++ b/GPTutor-Backend-v2/docs/internal-api.yaml @@ -0,0 +1,986 @@ +openapi: 3.0.3 +info: + title: GPTutor Internal API + version: 2.0.0 + description: | + # GPTutor Internal API + + Внутренние административные endpoints для управления системой GPTutor. + + **⚠️ ВНИМАНИЕ:** Этот API предназначен только для внутреннего использования администраторами системы. + + ## Доступные роуты + + ### Health Check + - `GET /health` - Проверка состояния сервера + + ### Аутентификация + - `GET /user` - Получить данные пользователя + - `POST /update-token` - Обновление API токена пользователя + + ### Модели + - `GET /v1/models` - Получение списка доступных моделей + + ### Chat Completions + - `POST /v1/chat/completions` - Создание chat completion + + ### Files + - `POST /upload` - Загрузка файла в S3 + - `GET /files` - Получение списка файлов пользователя + - `DELETE /files/:fileId` - Удаление файла + + ## Безопасность + + Все endpoints требуют соответствующей авторизации: + - **VK Auth** - для административных операций + - **API Key** - для пользовательских операций + +servers: + - url: http://localhost:3001 + description: Development server + - url: https://api.gptutor.site + description: Production server + +paths: + /health: + get: + tags: [System] + summary: Health Check + description: | + Проверяет состояние сервера и возвращает базовую информацию о системе. + + **Использование:** + - Мониторинг состояния сервера + - Проверка доступности API + - Load balancer health checks + responses: + '200': + description: Сервер работает нормально + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: true + data: + type: object + properties: + status: + type: string + example: "ok" + description: "Статус сервера" + timestamp: + type: string + format: date-time + example: "2024-01-15T10:30:00Z" + description: "Время проверки" + examples: + healthy: + summary: Сервер работает + value: + success: true + data: + status: "ok" + timestamp: "2024-01-15T10:30:00Z" + '500': + description: Внутренняя ошибка сервера + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /user: + get: + tags: [Authentication] + summary: Получить данные пользователя + description: | + Получает информацию о пользователе, включая баланс, API ключ и данные VK. + + **Использование:** + - Получение данных пользователя + - Проверка баланса + - Получение API ключа + - Просмотр VK данных пользователя + + **Требует:** VK подпись в заголовке Authorization + security: + - VkAuth: [] + responses: + '200': + description: Данные пользователя получены успешно + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: true + data: + type: object + properties: + message: + type: string + example: "VK authorization successful!" + vkData: + type: object + description: "Данные от VK" + properties: + vk_user_id: + type: string + vk_app_id: + type: string + vk_is_app_user: + type: string + vk_language: + type: string + vk_platform: + type: string + vk_ts: + type: string + dbUser: + type: object + properties: + id: + type: string + description: "ID пользователя в БД" + vkId: + type: string + description: "VK ID пользователя" + balance: + type: number + description: "Баланс пользователя" + apiKey: + type: string + description: "API ключ пользователя" + isActive: + type: boolean + description: "Активен ли пользователь" + createdAt: + type: string + format: date-time + description: "Дата создания" + updatedAt: + type: string + format: date-time + description: "Дата обновления" + timestamp: + type: string + format: date-time + examples: + success: + summary: Успешная VK авторизация + value: + success: true + data: + message: "VK authorization successful!" + vkData: + vk_user_id: "123456789" + vk_app_id: "51602327" + vk_is_app_user: "1" + vk_language: "ru" + vk_platform: "web" + vk_ts: "1642234567" + dbUser: + id: "user_abc123" + vkId: "123456789" + balance: 10.5 + apiKey: "sk-abc123..." + isActive: true + createdAt: "2024-01-01T00:00:00Z" + updatedAt: "2024-01-15T10:30:00Z" + timestamp: "2024-01-15T10:30:00Z" + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalError' + + /update-token: + post: + tags: [Authentication] + summary: Обновление API токена + description: | + Генерирует новый API ключ для пользователя. Старый ключ становится недействительным. + + **Использование:** + - Обновление скомпрометированного API ключа + - Ротация ключей безопасности + - Административное управление пользователями + + **Требует:** VK авторизацию + security: + - VkAuth: [] + responses: + '200': + description: Токен успешно обновлен + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: true + data: + type: object + properties: + message: + type: string + example: "API token updated successfully!" + newApiKey: + type: string + description: "Новый API ключ" + example: "sk-abc123def456ghi789jkl012mno345pqr678stu901vwx234yz567890" + user: + type: object + properties: + id: + type: string + description: "ID пользователя" + vkId: + type: string + description: "VK ID пользователя" + balance: + type: number + description: "Баланс пользователя" + isActive: + type: boolean + description: "Активен ли пользователь" + updatedAt: + type: string + format: date-time + description: "Время обновления" + timestamp: + type: string + format: date-time + examples: + success: + summary: Успешное обновление токена + value: + success: true + data: + message: "API token updated successfully!" + newApiKey: "sk-abc123def456ghi789jkl012mno345pqr678stu901vwx234yz567890" + user: + id: "user_123" + vkId: "vk_456" + balance: 10.5 + isActive: true + updatedAt: "2024-01-15T10:30:00Z" + timestamp: "2024-01-15T10:30:00Z" + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalError' + + /v1/models: + get: + tags: [Models] + summary: Получение списка моделей + description: | + Возвращает список всех доступных AI моделей с информацией о ценах и провайдерах. + + **Использование:** + - Получение актуального списка моделей + - Проверка доступности моделей + - Анализ цен на модели + + **Особенности:** + - Модели сортируются по цене (самые дорогие сверху) + - Включает информацию о провайдерах + - Показывает время последнего обновления + responses: + '200': + description: Список моделей успешно получен + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: true + data: + type: object + properties: + models: + type: array + items: + $ref: '#/components/schemas/Model' + description: "Список доступных моделей" + total: + type: integer + description: "Общее количество моделей" + example: 150 + providers: + type: array + items: + type: string + description: "Список провайдеров" + example: ["x-ai", "deepseek", "google", "qwen", "perplexity", "mistralai", "openai"] + lastUpdated: + type: string + format: date-time + description: "Время последнего обновления" + examples: + success: + summary: Успешное получение моделей + value: + success: true + data: + models: + - id: "openai/gpt-4o" + name: "GPT-4o" + description: "Most capable GPT-4 model" + pricing_rub: + prompt: 0.15 + completion: 0.6 + context_length: 128000 + created: 1642234567 + total: 150 + providers: ["openai", "anthropic", "google", "deepseek"] + lastUpdated: "2024-01-15T10:30:00Z" + '503': + description: Сервис моделей недоступен + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + service_unavailable: + summary: Сервис недоступен + value: + error: "Models service not ready" + '500': + $ref: '#/components/responses/InternalError' + + /v1/chat/completions: + post: + tags: [Chat Completions] + summary: Создание chat completion + description: | + Создает completion для чата с поддержкой streaming и автоматическим выбором модели. + + **Использование:** + - Генерация ответов от AI + - Streaming режим для реального времени + - Автоматический fallback между моделями + + **Особенности:** + - Поддержка streaming режима + - Автоматический расчет стоимости + - Fallback на другие модели при ошибках + - Поддержка всех параметров OpenAI API + security: + - ApiKeyAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ChatCompletionRequest' + examples: + simple: + summary: Простой запрос + value: + model: "google/gemini-2.5-flash-lite" + messages: + - role: "user" + content: "Привет! Как дела?" + streaming: + summary: Streaming запрос + value: + model: "google/gemini-2.5-flash-lite" + messages: + - role: "user" + content: "Расскажи историю" + stream: true + max_tokens: 150 + advanced: + summary: Продвинутый запрос + value: + model: "anthropic/claude-3-haiku" + messages: + - role: "user" + content: "Объясни квантовую физику простыми словами" + max_tokens: 500 + temperature: 0.7 + responses: + '200': + description: Успешный ответ + content: + application/json: + schema: + $ref: '#/components/schemas/ChatCompletionResponse' + text/event-stream: + schema: + type: string + description: | + Streaming ответ в формате Server-Sent Events: + ``` + data: {"choices":[{"delta":{"content":"Hello"}}]} + data: {"choices":[{"delta":{"content":" there!"}}]} + data: [DONE] + ``` + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '402': + description: Недостаточно средств + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + insufficient_balance: + summary: Недостаточно средств + value: + error: "Insufficient balance" + '500': + $ref: '#/components/responses/InternalError' + + /upload: + post: + tags: [Files] + summary: Загрузка файла в S3 + description: | + Загружает файл в Yandex Object Storage с автоматической оптимизацией. + + **Использование:** + - Загрузка изображений с оптимизацией + - Загрузка документов с сжатием + - Загрузка текстовых файлов + + **Особенности:** + - Автоматическая оптимизация изображений (качество 60%) + - Сжатие PDF файлов + - Поддержка файлов до 50MB + - Генерация уникальных имен файлов + + **Поддерживаемые типы:** + - **Изображения:** jpg, jpeg, png, gif, bmp, svg, webp, tiff + - **Документы:** pdf, doc, docx, xls, xlsx, csv + - **Текстовые:** txt, js, html, css, json, xml, md, log, py, java, c, cpp, sql + security: + - VkAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + description: "Файл для загрузки" + examples: + image: + summary: Загрузка изображения + value: + file: "image.jpg" + document: + summary: Загрузка документа + value: + file: "document.pdf" + responses: + '200': + description: Файл успешно загружен + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: true + data: + type: object + properties: + message: + type: string + example: "File uploaded successfully!" + file: + type: object + properties: + id: + type: string + description: "ID файла в БД" + name: + type: string + description: "Оригинальное имя файла" + type: + type: string + description: "MIME тип файла" + url: + type: string + description: "URL файла в S3" + size: + type: integer + description: "Размер файла в байтах" + createdAt: + type: string + format: date-time + description: "Время загрузки" + timestamp: + type: string + format: date-time + examples: + success: + summary: Успешная загрузка + value: + success: true + data: + message: "File uploaded successfully!" + file: + id: "file_abc123" + name: "document.pdf" + type: "application/pdf" + url: "https://gptutor-bucket.storage.yandexcloud.net/uuid.pdf" + size: 1024000 + createdAt: "2024-01-15T10:30:00Z" + timestamp: "2024-01-15T10:30:00Z" + '400': + description: Неверный запрос + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + no_file: + summary: Файл не предоставлен + value: + error: "No file provided" + unsupported_type: + summary: Неподдерживаемый тип файла + value: + error: "Unsupported file type" + '401': + $ref: '#/components/responses/Unauthorized' + '413': + description: Файл слишком большой + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + file_too_large: + summary: Файл превышает лимит + value: + error: "File too large. Maximum size is 50MB" + '500': + $ref: '#/components/responses/InternalError' + + /files: + get: + tags: [Files] + summary: Получение списка файлов пользователя + description: | + Возвращает список всех файлов, загруженных пользователем. + + **Использование:** + - Просмотр загруженных файлов + - Получение метаданных файлов + - Управление файлами пользователя + security: + - VkAuth: [] + responses: + '200': + description: Список файлов успешно получен + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: true + data: + type: object + properties: + message: + type: string + example: "Files retrieved successfully!" + files: + type: array + items: + type: object + properties: + id: + type: string + description: "ID файла" + name: + type: string + description: "Имя файла" + type: + type: string + description: "MIME тип" + url: + type: string + description: "URL файла" + size: + type: integer + description: "Размер в байтах" + createdAt: + type: string + format: date-time + description: "Время загрузки" + total: + type: integer + description: "Общее количество файлов" + timestamp: + type: string + format: date-time + examples: + success: + summary: Список файлов + value: + success: true + data: + message: "Files retrieved successfully!" + files: + - id: "file_abc123" + name: "document.pdf" + type: "application/pdf" + url: "https://gptutor-bucket.storage.yandexcloud.net/uuid.pdf" + size: 1024000 + createdAt: "2024-01-15T10:30:00Z" + - id: "file_def456" + name: "image.jpg" + type: "image/jpeg" + url: "https://gptutor-bucket.storage.yandexcloud.net/uuid2.jpg" + size: 512000 + createdAt: "2024-01-15T11:00:00Z" + total: 2 + timestamp: "2024-01-15T11:30:00Z" + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalError' + + /files/{fileId}: + delete: + tags: [Files] + summary: Удаление файла + description: | + Удаляет файл из S3 и базы данных. + + **Использование:** + - Удаление ненужных файлов + - Освобождение места в хранилище + - Управление файлами пользователя + + **Безопасность:** + - Пользователь может удалять только свои файлы + - Файл удаляется как из S3, так и из БД + security: + - VkAuth: [] + parameters: + - name: fileId + in: path + required: true + description: "ID файла для удаления" + schema: + type: string + example: "file_abc123" + responses: + '200': + description: Файл успешно удален + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: true + data: + type: object + properties: + message: + type: string + example: "File deleted successfully!" + fileId: + type: string + description: "ID удаленного файла" + timestamp: + type: string + format: date-time + examples: + success: + summary: Успешное удаление + value: + success: true + data: + message: "File deleted successfully!" + fileId: "file_abc123" + timestamp: "2024-01-15T10:30:00Z" + '401': + $ref: '#/components/responses/Unauthorized' + '403': + description: Доступ запрещен + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + access_denied: + summary: Доступ запрещен + value: + error: "Access denied" + '404': + description: Файл не найден + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + not_found: + summary: Файл не найден + value: + error: "File not found" + '500': + $ref: '#/components/responses/InternalError' + +components: + securitySchemes: + ApiKeyAuth: + type: http + scheme: bearer + description: | + API ключ пользователя для доступа к LLM endpoints. + Формат: Bearer sk-... + VkAuth: + type: http + scheme: bearer + description: | + VK подпись для авторизации через VK приложение. + Передается в заголовке Authorization как Bearer token. + + schemas: + Model: + type: object + properties: + id: + type: string + description: "Идентификатор модели" + example: "openai/gpt-4o" + name: + type: string + description: "Название модели" + example: "GPT-4o" + description: + type: string + description: "Описание модели" + example: "Most capable GPT-4 model" + pricing_rub: + type: object + properties: + prompt: + type: number + description: "Цена за входные токены (руб/1K токенов)" + example: 0.15 + completion: + type: number + description: "Цена за выходные токены (руб/1K токенов)" + example: 0.6 + context_length: + type: integer + description: "Максимальная длина контекста" + example: 128000 + created: + type: integer + description: "Время создания модели (Unix timestamp)" + example: 1642234567 + + ChatCompletionRequest: + type: object + required: [messages] + properties: + model: + type: string + default: "google/gemini-2.5-flash-lite" + example: "google/gemini-2.5-flash-lite" + description: | + Модель для генерации ответа. Поддерживаются все модели OpenRouter. + + **Популярные модели:** + - `google/gemini-2.5-flash-lite` - быстрая и дешевая + - `anthropic/claude-3-haiku` - сбалансированная + - `openai/gpt-4o-mini` - OpenAI модель + messages: + type: array + items: + $ref: '#/components/schemas/ChatMessage' + description: "Массив сообщений для контекста" + max_tokens: + type: integer + minimum: 1 + maximum: 32000 + description: "Максимальное количество токенов в ответе" + example: 150 + temperature: + type: number + minimum: 0 + maximum: 2 + description: "Креативность ответа (0.0-2.0)" + example: 0.7 + top_p: + type: number + minimum: 0 + maximum: 1 + description: "Nucleus sampling parameter" + example: 0.9 + frequency_penalty: + type: number + minimum: -2 + maximum: 2 + description: "Штраф за частоту повторений" + example: 0 + presence_penalty: + type: number + minimum: -2 + maximum: 2 + description: "Штраф за присутствие токенов" + example: 0 + stop: + type: array + items: + type: string + description: "Стоп-слова для завершения генерации" + example: ["\n\n", "Human:", "Assistant:"] + stream: + type: boolean + description: "Включить streaming режим" + example: false + + ChatMessage: + type: object + required: [role, content] + properties: + role: + type: string + enum: [system, user, assistant] + description: "Роль отправителя сообщения" + example: "user" + content: + type: string + description: "Содержимое сообщения" + example: "Привет! Как дела?" + + ChatCompletionResponse: + type: object + properties: + id: + type: string + description: "Уникальный идентификатор completion" + example: "chatcmpl-abc123" + object: + type: string + description: "Тип объекта" + example: "chat.completion" + created: + type: integer + description: "Время создания (Unix timestamp)" + example: 1677652288 + model: + type: string + description: "Использованная модель" + example: "google/gemini-2.5-flash-lite" + choices: + type: array + items: + $ref: '#/components/schemas/Choice' + usage: + $ref: '#/components/schemas/Usage' + + Choice: + type: object + properties: + index: + type: integer + description: "Индекс выбора" + example: 0 + message: + $ref: '#/components/schemas/ChatMessage' + finish_reason: + type: string + enum: [stop, length, content_filter, null] + description: "Причина завершения" + example: "stop" + + Usage: + type: object + properties: + prompt_tokens: + type: integer + description: "Количество входных токенов" + example: 4 + completion_tokens: + type: integer + description: "Количество выходных токенов" + example: 147 + total_tokens: + type: integer + description: "Общее количество токенов" + example: 151 + cost: + type: number + description: "Стоимость запроса в рублях" + example: 0.0000592 + + Error: + type: object + properties: + error: + type: string + description: "Описание ошибки" + example: "Invalid request" + + responses: + BadRequest: + description: Неверный запрос + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + missing_messages: + summary: Отсутствует массив messages + value: + error: "messages array is required" + invalid_model: + summary: Неверная модель + value: + error: "Invalid model specified" + + Unauthorized: + description: Неавторизованный доступ + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + invalid_api_key: + summary: Неверный API ключ + value: + error: "Invalid API key or inactive user" + missing_api_key: + summary: Отсутствует API ключ + value: + error: "Missing or invalid Authorization header" + + InternalError: + description: Внутренняя ошибка сервера + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + server_error: + summary: Ошибка сервера + value: + error: "Internal server error" diff --git a/GPTutor-Backend-v2/docs/internal.html b/GPTutor-Backend-v2/docs/internal.html new file mode 100644 index 00000000..07b5bf38 --- /dev/null +++ b/GPTutor-Backend-v2/docs/internal.html @@ -0,0 +1,276 @@ + + + + + + GPTutor Internal API Documentation + + + +
+

🔧 GPTutor Internal API

+

Внутренние административные endpoints

+
+ +
+ ВНИМАНИЕ: Этот API предназначен только для внутреннего использования администраторами системы +
+ +
+
+

📋 Обзор API

+

Внутренний API GPTutor предоставляет административные endpoints для управления системой, мониторинга состояния и отладки.

+

Базовый URL: http://localhost:3001 (dev) / https://api.gptutor.site (prod)

+

Версия: 2.0.0

+
+ +
+

🔐 Авторизация

+

VK Auth: Для административных операций требуется VK подпись в заголовке Authorization

+

API Key: Для пользовательских операций требуется API ключ в формате Bearer sk-...

+
+ +

🚀 Доступные Endpoints

+ +
+
+
GET
+
/health
+
+ Проверка состояния сервера. Используется для мониторинга и health checks. +
+
+ +
+
GET
+
/user
+
+ Получить данные пользователя. Получение информации о пользователе, включая баланс и API ключ. +
+
+ +
+
POST
+
/update-token
+
+ Обновление API токена пользователя. Генерирует новый ключ и делает старый недействительным. +
+
+ +
+
GET
+
/v1/models
+
+ Получение списка доступных AI моделей с информацией о ценах и провайдерах. +
+
+ +
+
POST
+
/v1/chat/completions
+
+ Создание chat completion с поддержкой streaming и автоматическим выбором модели. +
+
+ +
+
POST
+
/upload
+
+ Загрузка файла в S3 с автоматической оптимизацией изображений и сжатием документов. +
+
+ +
+
GET
+
/files
+
+ Получение списка всех файлов пользователя с метаданными. +
+
+ +
+
DELETE
+
/files/:fileId
+
+ Удаление файла из S3 и базы данных. Пользователь может удалять только свои файлы. +
+
+
+ +
+ + +
+
+ + + + diff --git a/GPTutor-Backend-v2/docs/openapi.yaml b/GPTutor-Backend-v2/docs/openapi.yaml new file mode 100644 index 00000000..18a36b17 --- /dev/null +++ b/GPTutor-Backend-v2/docs/openapi.yaml @@ -0,0 +1,1820 @@ +openapi: 3.0.3 +info: + title: Giga Router API + version: 1.0.1 + description: | + # Giga Router API + + Giga Router API предоставляет единый API для доступа к различным AI-моделям через один endpoint. + + ## Быстрый старт + + ### Использование с OpenAI SDK + + **Python:** + ```python + from openai import OpenAI + + client = OpenAI( + base_url="https://api.giga-router.ru/v1", + api_key="your-api-key-here" + ) + + completion = client.chat.completions.create( + model="google/gemini-2.5-flash-lite", + messages=[ + {"role": "user", "content": "Привет! Как дела?"} + ] + ) + + print(completion.choices[0].message.content) + ``` + + **JavaScript:** + ```javascript + import OpenAI from 'openai'; + + const openai = new OpenAI({ + baseURL: 'https://api.giga-router.ru/v1', + apiKey: 'your-api-key-here' + }); + + const completion = await openai.chat.completions.create({ + model: 'google/gemini-2.5-flash-lite', + messages: [ + { role: 'user', content: 'Привет! Как дела?' } + ] + }); + + console.log(completion.choices[0].message.content); + ``` + + # Мультимодальные запросы + + ## Работа с изображениями + + Giga Router API поддерживает отправку изображений к мультимодальным моделям через endpoint `/v1/chat/completions`. Вы можете отправлять изображения двумя способами: через прямые URL или в base64-кодированном виде. + + **Основные возможности:** + - Анализ фотографий и скриншотов + - Описание содержимого изображений + - Извлечение текста с картинок (OCR) + - Сравнение изображений + - Ответы на вопросы по изображениям + + **Поддерживаемые форматы:** + - PNG, JPEG, WebP, GIF + - Максимальный размер: до 20MB + - Можно отправлять несколько изображений в одном запросе + + ### Отправка изображений через URL + + Самый простой способ - использовать прямые ссылки на изображения в интернете: + + **Python:** + ```python + from openai import OpenAI + + client = OpenAI( + base_url="https://api.giga-router.ru/v1", + api_key="your-api-key-here" + ) + + completion = client.chat.completions.create( + model="google/gemini-2.5-flash-lite", + messages=[ + { + "role": "user", + "content": [ + {"type": "text", "text": "Что изображено на этой картинке?"}, + { + "type": "image_url", + "image_url": { + "url": "https://storage.yandexcloud.net/giga-router/test-photo-router.jpg", + } + } + ] + } + ] + ) + + print(completion.choices[0].message.content) + ``` + + **JavaScript с Base64 изображением:** + ```javascript + import OpenAI from 'openai'; + + const openai = new OpenAI({ + baseURL: 'https://api.giga-router.ru/v1', + apiKey: 'your-api-key-here' + }); + + // Конвертация файла в base64 + const fs = require('fs'); + const imageBuffer = fs.readFileSync('path/to/image.jpg'); + const base64Image = imageBuffer.toString('base64'); + const dataUrl = `data:image/jpeg;base64,${base64Image}`; + + const completion = await openai.chat.completions.create({ + model: 'google/gemini-2.5-flash-lite', + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: 'Проанализируй это изображение' }, + { + type: 'image_url', + image_url: { + url: dataUrl, + } + } + ] + } + ] + }); + + console.log(completion.choices[0].message.content); + ``` + + ## Работа с PDF документами + + Giga Router API поддерживает обработку PDF документов через endpoint `/v1/chat/completions`. PDF файлы можно отправлять как прямые URL, так и в base64-кодированном виде. Эта функция работает с **любыми** моделями на Giga Router. + + **Основные возможности:** + - Извлечение текста из PDF документов + - Анализ структуры документа + - Поиск информации в многостраничных документах + - Суммаризация содержимого + - Ответы на вопросы по содержимому PDF + - Работа со сканированными документами (OCR) + + **Поддерживаемые форматы:** + - PDF документы любого размера + - Сканированные PDF (с OCR) + - Многостраничные документы + - Документы с изображениями и таблицами + + ### Отправка PDF через URL + + Для публично доступных PDF документов можно использовать прямые ссылки: + + **Python:** + ```python + from openai import OpenAI + + client = OpenAI( + base_url="https://api.giga-router.ru/v1", + api_key="your-api-key-here" + ) + + # Отправляем PDF по URL + completion = client.chat.completions.create( + model="google/gemini-2.5-flash-lite", + messages=[ + { + "role": "user", + "content": [ + {"type": "text", "text": "Проанализируй этот PDF документ"}, + { + "type": "file", + "file": { + "filename": "document.pdf", + "file_data": "https://storage.yandexcloud.net/giga-router/181_I-test.pdf" + } + } + ] + } + ] + ) + + print(completion.choices[0].message.content) + ``` + + **JavaScript:** + ```javascript + const response = await fetch('https://api.giga-router.ru/v1/chat/completions', { + method: 'POST', + headers: { + 'Authorization': 'Bearer ваш-api-ключ', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model: 'google/gemini-2.5-flash-lite', + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: 'Проанализируй этот PDF документ' }, + { + type: 'file', + file: { + filename: 'document.pdf', + file_data: 'https://storage.yandexcloud.net/giga-router/181_I-test.pdf' + } + } + ] + } + ] + }) + }); + + const data = await response.json(); + console.log(data); + ``` + + ### Отправка локальных PDF файлов (Base64) + + Для PDF файлов, хранящихся локально, используйте base64-кодирование: + + **Python:** + ```python + from openai import OpenAI + import base64 + + def encode_pdf_to_base64(pdf_path): + """Конвертирует PDF в base64""" + with open(pdf_path, "rb") as pdf_file: + return base64.b64encode(pdf_file.read()).decode('utf-8') + + client = OpenAI( + base_url="https://api.giga-router.ru/v1", + api_key="your-api-key-here" + ) + + # Кодируем локальный PDF + pdf_path = "путь/к/документу.pdf" + base64_pdf = encode_pdf_to_base64(pdf_path) + data_url = f"data:application/pdf;base64,{base64_pdf}" + + # Отправляем запрос + completion = client.chat.completions.create( + model="google/gemini-2.5-flash-lite", + messages=[ + { + "role": "user", + "content": [ + {"type": "text", "text": "Что содержится в этом документе?"}, + { + "type": "file", + "file": { + "filename": "document.pdf", + "file_data": data_url + } + } + ] + } + ] + ) + + print(completion.choices[0].message.content) + ``` + + **JavaScript:** + ```javascript + // Функция для конвертации PDF в base64 + function encodePDFToBase64(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = reject; + reader.readAsDataURL(file); + }); + } + + // Использование с input элементом + const fileInput = document.getElementById('pdfInput'); + const file = fileInput.files[0]; + + if (file) { + const base64PDF = await encodePDFToBase64(file); + + const response = await fetch('https://api.giga-router.ru/v1/chat/completions', { + method: 'POST', + headers: { + 'Authorization': 'Bearer ваш-api-ключ', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model: 'google/gemini-2.5-flash-lite', + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: 'Проанализируй этот PDF' }, + { + type: 'file', + file: { + filename: file.name, + file_data: base64PDF + } + } + ] + } + ] + }) + }); + + const data = await response.json(); + console.log(data); + } + ``` + + ### Оптимизация работы с PDF + + **Повторное использование аннотаций:** + + При первом запросе с PDF API возвращает аннотации файла. Их можно сохранить и использовать в последующих запросах, чтобы избежать повторной обработки документа: + + ```python + # Первый запрос - получаем аннотации + response = client.chat.completions.create( + model="google/gemini-2.5-flash-lite", + messages=[...] + ) + + # Сохраняем аннотации + annotations = response.choices[0].message.annotations + + # Последующие запросы с аннотациями (без повторной обработки PDF) + follow_up = client.chat.completions.create( + model="google/gemini-2.5-flash-lite", + messages=[ + {"role": "user", "content": "Дай краткое резюме"}, + {"role": "assistant", "content": "...", "annotations": annotations}, + {"role": "user", "content": "Какие ключевые моменты?"} + ] + ) + ``` + + ### Типы обработки PDF + + - **Автоматическая обработка** - API сам выбирает оптимальный метод + - **Текстовая обработка** - для документов с четким текстом (бесплатно) + - **OCR обработка** - для сканированных документов (платно) + - **Нативная обработка** - для моделей с встроенной поддержкой файлов + + ### Поддерживаемые возможности + + - **Извлечение текста** из любых PDF документов + - **Анализ структуры** - заголовки, параграфы, списки + - **Работа с таблицами** - извлечение данных из таблиц + - **OCR для сканированных документов** - распознавание текста с изображений + - **Многостраничные документы** - обработка документов любой длины + - **Смешанный контент** - текст, изображения, таблицы в одном документе + + ## Генерация изображений + + Giga Router API поддерживает генерацию изображений через модели с поддержкой модальности `"image"` в `output_modalities`. Эти модели могут создавать изображения из текстовых описаний. + + **Основные возможности:** + - Генерация изображений по текстовому описанию + - Создание изображений в различных стилях + - Поддержка streaming для генерации + - Множественная генерация изображений + - Высокое качество и детализация + + **Поддерживаемые форматы:** + - PNG изображения в base64 формате + - Различные размеры и разрешения + - Высокое качество генерации + + ### Базовая генерация изображений + + Для генерации изображений используйте параметр `modalities` с включением `"image"` и `"text"`: + + **Python:** + ```python + from openai import OpenAI + + client = OpenAI( + base_url="https://api.giga-router.ru/v1", + api_key="your-api-key-here" + ) + + # Генерируем изображение + completion = client.chat.completions.create( + model="google/gemini-2.5-flash-image-preview", + messages=[ + { + "role": "user", + "content": "Создай красивое изображение заката над горами" + } + ], + modalities=["image", "text"] + ) + + # Получаем сгенерированное изображение + if completion.choices[0].message.images: + for image in completion.choices[0].message.images: + image_url = image["image_url"]["url"] # Base64 data URL + print(f"Сгенерированное изображение: {image_url[:50]}...") + ``` + + **JavaScript:** + ```javascript + const response = await fetch('https://api.giga-router.ru/v1/chat/completions', { + method: 'POST', + headers: { + 'Authorization': 'Bearer ваш-api-ключ', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model: 'google/gemini-2.5-flash-image-preview', + messages: [ + { + role: 'user', + content: 'Создай красивое изображение заката над горами' + } + ], + modalities: ['image', 'text'] + }) + }); + + const result = await response.json(); + + // Получаем сгенерированное изображение + if (result.choices) { + const message = result.choices[0].message; + if (message.images) { + message.images.forEach((image, index) => { + const imageUrl = image.image_url.url; // Base64 data URL + console.log(`Сгенерированное изображение ${index + 1}: ${imageUrl.substring(0, 50)}...`); + }); + } + } + ``` + + ### Streaming генерация изображений + + Генерация изображений также поддерживает streaming режим: + + **Python:** + ```python + import requests + import json + + url = "https://api.giga-router.ru/v1/chat/completions" + headers = { + "Authorization": "Bearer ваш-api-ключ", + "Content-Type": "application/json" + } + + payload = { + "model": "google/gemini-2.5-flash-image-preview", + "messages": [ + { + "role": "user", + "content": "Создай изображение футуристического города" + } + ], + "modalities": ["image", "text"], + "stream": True + } + + response = requests.post(url, headers=headers, json=payload, stream=True) + + for line in response.iter_lines(): + if line: + line = line.decode('utf-8') + if line.startswith('data: '): + data = line[6:] + if data != '[DONE]': + try: + chunk = json.loads(data) + if chunk.get("choices"): + delta = chunk["choices"][0].get("delta", {}) + if delta.get("images"): + for image in delta["images"]: + print(f"Сгенерированное изображение: {image['image_url']['url'][:50]}...") + except json.JSONDecodeError: + continue + ``` + + **JavaScript:** + ```javascript + const response = await fetch('https://api.giga-router.ru/v1/chat/completions', { + method: 'POST', + headers: { + 'Authorization': 'Bearer ваш-api-ключ', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model: 'google/gemini-2.5-flash-image-preview', + messages: [ + { + role: 'user', + content: 'Создай изображение футуристического города' + } + ], + modalities: ['image', 'text'], + stream: true + }) + }); + + const reader = response.body?.getReader(); + const decoder = new TextDecoder(); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value); + const lines = chunk.split('\n'); + + for (const line of lines) { + if (line.startsWith('data: ')) { + const data = line.slice(6); + if (data !== '[DONE]') { + try { + const parsed = JSON.parse(data); + if (parsed.choices) { + const delta = parsed.choices[0].delta; + if (delta?.images) { + delta.images.forEach((image, index) => { + console.log(`Сгенерированное изображение ${index + 1}: ${image.image_url.url.substring(0, 50)}...`); + }); + } + } + } catch (e) { + // Пропускаем невалидный JSON + } + } + } + } + } + ``` + + ### Формат ответа + + При генерации изображений ответ содержит поле `images` с сгенерированными изображениями: + + ```json + { + "choices": [ + { + "message": { + "role": "assistant", + "content": "Я создал для вас красивое изображение заката.", + "images": [ + { + "type": "image_url", + "image_url": { + "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..." + } + } + ] + } + } + ] + } + ``` + + ### Совместимость моделей + + Не все модели поддерживают генерацию изображений. Для использования этой функции: + + 1. **Проверьте Output Modalities** - убедитесь, что модель имеет `"image"` в `output_modalities` + 2. **Установите параметр modalities** - включите `"modalities": ["image", "text"]` в запросе + 3. **Используйте совместимые модели** - например, `google/gemini-2.5-flash-image-preview` + + ### Лучшие практики + + - **Четкие промпты** - предоставляйте детальные описания для лучшего качества + - **Выбор модели** - выбирайте модели, специально предназначенные для генерации изображений + - **Обработка ошибок** - проверяйте наличие поля `images` в ответах + - **Хранение** - продумайте, как обрабатывать и хранить base64 данные изображений + + ## Работа с аудио файлами + + Giga Router API поддерживает отправку аудио файлов к совместимым моделям через endpoint `/v1/chat/completions`. Аудио файлы должны быть закодированы в base64 формате. + + **Основные возможности:** + - Транскрипция аудио в текст + - Анализ речи и интонации + - Извлечение информации из аудио + - Обработка голосовых команд + - Анализ эмоций в речи + + **Поддерживаемые форматы:** + - WAV - высокое качество, без сжатия + - MP3 - сжатый формат, меньший размер + - Максимальный размер: до 25MB + + **Важно:** Аудио файлы должны быть **base64-кодированными** - прямые URL не поддерживаются для аудио контента. + + ### Отправка аудио файлов + + Для отправки аудио файлов используйте тип контента `input_audio`: + + **Python:** + ```python + from openai import OpenAI + import base64 + + def encode_audio_to_base64(audio_path): + """Конвертирует аудио в base64""" + with open(audio_path, "rb") as audio_file: + return base64.b64encode(audio_file.read()).decode('utf-8') + + client = OpenAI( + base_url="https://api.giga-router.ru/v1", + api_key="your-api-key-here" + ) + + # Кодируем аудио файл + audio_path = "путь/к/аудио.wav" + base64_audio = encode_audio_to_base64(audio_path) + + # Отправляем запрос + completion = client.chat.completions.create( + model="google/gemini-2.5-flash", + messages=[ + { + "role": "user", + "content": [ + {"type": "text", "text": "Расшифруй этот аудио файл"}, + { + "type": "input_audio", + "input_audio": { + "data": base64_audio, + "format": "wav" + } + } + ] + } + ] + ) + + print(completion.choices[0].message.content) + ``` + + **JavaScript:** + ```javascript + // Функция для конвертации аудио в base64 + function encodeAudioToBase64(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result.split(',')[1]); // Убираем data:audio/wav;base64, + reader.onerror = reject; + reader.readAsDataURL(file); + }); + } + + // Использование с input элементом + const audioInput = document.getElementById('audioInput'); + const file = audioInput.files[0]; + + if (file) { + const base64Audio = await encodeAudioToBase64(file); + + const response = await fetch('https://api.giga-router.ru/v1/chat/completions', { + method: 'POST', + headers: { + 'Authorization': 'Bearer ваш-api-ключ', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model: 'google/gemini-2.5-flash', + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: 'Расшифруй этот аудио файл' }, + { + type: 'input_audio', + input_audio: { + data: base64Audio, + format: 'wav' + } + } + ] + } + ] + }) + }); + + const data = await response.json(); + console.log(data); + } + ``` + + ### Поддерживаемые форматы аудио + + - **WAV** - высокое качество, без сжатия, рекомендуется для точной транскрипции + - **MP3** - сжатый формат, меньший размер файла, подходит для длинных записей + + ### Совместимость моделей + + Не все модели поддерживают обработку аудио. Для использования этой функции: + + 1. **Проверьте Input Modalities** - убедитесь, что модель имеет `"audio"` в `input_modalities` + 2. **Используйте совместимые модели** - например, `google/gemini-2.5-flash` + 3. **Base64 кодирование** - аудио файлы должны быть закодированы в base64 + + ### Лучшие практики + + - **Качество аудио** - используйте WAV для лучшего качества транскрипции + - **Размер файла** - учитывайте ограничения по размеру (до 25MB) + - **Длительность** - короткие аудио файлы обрабатываются быстрее + - **Фоновый шум** - чистое аудио дает лучшие результаты + + ## Веб-поиск + + Giga Router API поддерживает интеграцию актуальных данных из интернета для любой модели через веб-поиск. Это позволяет получать свежую информацию и фактические данные в реальном времени. + + **Основные возможности:** + - Поиск актуальной информации в интернете + - Получение свежих новостей и данных + - Фактическая проверка информации + - Исследование тем с актуальными источниками + - Цитирование источников в ответах + + **Способы активации:** + - Добавление `:online` к названию модели + - Использование плагина `web` + - Настройка параметров поиска + + ### Активация веб-поиска + + **Способ 1: Модификатор :online** + + Самый простой способ - добавить `:online` к названию модели: + + **Python:** + ```python + from openai import OpenAI + + client = OpenAI( + base_url="https://api.giga-router.ru/v1", + api_key="your-api-key-here" + ) + + # Используем модель с веб-поиском + completion = client.chat.completions.create( + model="openai/gpt-4o:online", + messages=[ + { + "role": "user", + "content": "Какие последние новости о развитии ИИ?" + } + ] + ) + + print(completion.choices[0].message.content) + ``` + + **JavaScript:** + ```javascript + const response = await fetch('https://api.giga-router.ru/v1/chat/completions', { + method: 'POST', + headers: { + 'Authorization': 'Bearer ваш-api-ключ', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model: 'openai/gpt-4o:online', + messages: [ + { + role: 'user', + content: 'Какие последние новости о развитии ИИ?' + } + ] + }) + }); + + const data = await response.json(); + console.log(data); + ``` + + **Способ 2: Плагин web** + + Более гибкий способ - использование плагина `web`: + + **Python:** + ```python + from openai import OpenAI + + client = OpenAI( + base_url="https://api.giga-router.ru/v1", + api_key="your-api-key-here" + ) + + # Используем плагин web + completion = client.chat.completions.create( + model="openai/gpt-4o", + messages=[ + { + "role": "user", + "content": "Найди информацию о последних достижениях в квантовых вычислениях" + } + ], + plugins=[ + { + "id": "web", + "max_results": 3, + "search_prompt": "Вот актуальные результаты поиска:" + } + ] + ) + + print(completion.choices[0].message.content) + ``` + + **JavaScript:** + ```javascript + const response = await fetch('https://api.giga-router.ru/v1/chat/completions', { + method: 'POST', + headers: { + 'Authorization': 'Bearer ваш-api-ключ', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model: 'openai/gpt-4o', + messages: [ + { + role: 'user', + content: 'Найди информацию о последних достижениях в квантовых вычислениях' + } + ], + plugins: [ + { + id: 'web', + max_results: 3, + search_prompt: 'Вот актуальные результаты поиска:' + } + ] + }) + }); + + const data = await response.json(); + console.log(data); + ``` + + ### Настройка веб-поиска + + **Параметры плагина web:** + + ```json + { + "id": "web", + "engine": "exa", // "native", "exa" или не указано + "max_results": 5, // Максимум результатов (по умолчанию 5) + "search_prompt": "..." // Промпт для включения результатов + } + ``` + + **Выбор движка поиска:** + + - **`native`** - использует встроенные возможности провайдера + - **`exa`** - использует Exa API для поиска + - **не указано** - автоматический выбор (native для OpenAI/Anthropic, exa для остальных) + + ### Формат ответа с цитированием + + При использовании веб-поиска ответ содержит аннотации с источниками: + + ```json + { + "message": { + "role": "assistant", + "content": "Вот актуальная информация: ...", + "annotations": [ + { + "type": "url_citation", + "url_citation": { + "url": "https://example.com/news", + "title": "Заголовок статьи", + "content": "Содержимое статьи", + "start_index": 100, + "end_index": 200 + } + } + ] + } + } + ``` + + ### Стоимость веб-поиска + + **Exa поиск:** + - 800₽ за 1000 результатов + - По умолчанию 5 результатов = 4₽ за запрос + + **Нативный поиск:** + - Стоимость передается от провайдера + - Зависит от размера контекста поиска (low/medium/high) + + ### Размер контекста поиска + + Можно указать размер контекста поиска: + + ```json + { + "model": "openai/gpt-4o", + "messages": [...], + "web_search_options": { + "search_context_size": "high" // "low", "medium", "high" + } + } + ``` + + ### Лучшие практики + + - **Четкие запросы** - формулируйте конкретные вопросы для лучших результатов + - **Ограничение результатов** - используйте `max_results` для контроля стоимости + - **Проверка источников** - всегда проверяйте аннотации с источниками + - **Актуальность** - веб-поиск особенно полезен для свежих данных + + # Streaming + + API поддерживает streaming ответы от любой модели. Это полезно для создания чат-интерфейсов или других приложений, где UI должен обновляться по мере генерации ответа. + + **Python:** + ```python + import requests + import json + + response = requests.post( + 'https://api.giga-router.ru/v1/chat/completions', + headers={'Authorization': 'Bearer your-api-key-here'}, + json={ + 'model': 'google/gemini-2.5-flash-lite', + 'messages': [{'role': 'user', 'content': 'Расскажи историю'}], + 'stream': True + }, + stream=True + ) + + for line in response.iter_lines(): + if line: + line_text = line.decode('utf-8') + if line_text.startswith('data: '): + data = line_text[6:] + if data == '[DONE]': + break + try: + parsed = json.loads(data) + content = parsed['choices'][0]['delta'].get('content') + if content: + print(content, end='', flush=True) + except json.JSONDecodeError: + pass + ``` + + **JavaScript:** + ```javascript + const response = await fetch('https://api.giga-router.ru/v1/chat/completions', { + method: 'POST', + headers: { + 'Authorization': 'Bearer your-api-key-here', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model: 'google/gemini-2.5-flash-lite', + messages: [{ role: 'user', content: 'Расскажи историю' }], + stream: true + }) + }); + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value); + const lines = chunk.split('\n'); + + for (const line of lines) { + if (line.startsWith('data: ')) { + const data = line.slice(6); + if (data === '[DONE]') continue; + + try { + const parsed = JSON.parse(data); + const content = parsed.choices?.[0]?.delta?.content; + if (content) { + process.stdout.write(content); + } + } catch (e) { + // Игнорируем ошибки парсинга + } + } + } + } + ``` + + # Аутентификация + + API использует Bearer токены для аутентификации. Получите ваш API ключ в профиле приложения https://vk.com/app54187353#/profile + + ```bash + curl -H "Authorization: Bearer your-api-key-here" \ + https://api.giga-router.ru/v1/chat/completions + ``` + + # Стоимость + + Стоимость рассчитывается автоматически на основе токенов и списывается с баланса пользователя. Информация о стоимости включена в каждый ответ. + + license: + name: MIT + url: https://opensource.org/licenses/MIT + +servers: + - url: https://api.giga-router.ru/ + description: Production server + +tags: + - name: Chat Completions + description: OpenAI-совместимые chat completions с поддержкой streaming + - name: Мультимодальность + description: Работа с изображениями, PDF и другими типами контента + - name: Chat Models + description: Управление и получение информации о доступных моделях LLM + - name: Files + description: Управление файлами - загрузка, удаление и получение списка файлов пользователя + +paths: + /v1/chat/completions: + post: + tags: [Chat Completions] + summary: Создание chat completion + description: | + Создание chat completion с поддержкой streaming. + + ## Параметры + + - `model` - модель для генерации (по умолчанию: google/gemini-2.5-flash-lite) + - `messages` - массив сообщений для контекста + - `stream` - включить streaming режим + - `max_tokens` - максимальное количество токенов + - `temperature` - креативность ответа (0.0-1.0) + + ## Мультимодальность + + API поддерживает отправку различных типов контента вместе с текстом для создания более богатых и интерактивных запросов. + + ### Изображения + + API поддерживает отправку изображений через параметр `messages`. Изображения можно отправлять как URL, так и в base64-кодированном виде. Поддерживаются форматы: PNG, JPEG, WebP, GIF. + + ### PDF + + API поддерживает обработку PDF документов через параметр `messages`. PDF можно отправлять как URL, так и в base64-кодированном виде. Поддерживается извлечение текста, анализ структуры и OCR для сканированных документов. + + ### Аудио + + API поддерживает обработку аудио файлов через параметр `messages`. Аудио файлы должны быть в base64-кодированном виде. Поддерживается транскрипция, анализ речи и извлечение информации из аудио. + + ### Веб-поиск + + API поддерживает интеграцию актуальных данных из интернета через веб-поиск. Активируется добавлением `:online` к модели или использованием плагина `web`. Поддерживается поиск свежей информации с цитированием источников. + + ## Streaming + + При `stream: true` ответ приходит в формате Server-Sent Events: + + ``` + data: {"choices":[{"delta":{"content":"Hello"}}]} + data: {"choices":[{"delta":{"content":" there!"}}]} + data: [DONE] + ``` + + ## Стоимость + + Стоимость рассчитывается автоматически и списывается с баланса пользователя. Информация о стоимости включена в каждый ответ. + security: + - ApiKeyAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ChatCompletionRequest' + examples: + simple: + summary: Простой запрос + value: + model: "google/gemini-2.5-flash-lite" + messages: + - role: "user" + content: "Привет! Как дела?" + streaming: + summary: Streaming запрос + value: + model: "google/gemini-2.5-flash-lite" + messages: + - role: "user" + content: "Расскажи историю" + stream: true + max_tokens: 150 + advanced: + summary: Продвинутый запрос + value: + model: "anthropic/claude-3-haiku" + messages: + - role: "user" + content: "Объясни квантовую физику простыми словами" + max_tokens: 500 + temperature: 0.7 + image: + summary: Запрос с изображением + value: + model: "google/gemini-2.5-flash-lite" + messages: + - role: "user" + content: + - type: "text" + text: "Что изображено на этой картинке?" + - type: "image_url" + image_url: + url: "https://storage.yandexcloud.net/giga-router/test-photo-router.jpg" + detail: "high" + pdf: + summary: Запрос с PDF документом + value: + model: "google/gemini-2.5-flash-lite" + messages: + - role: "user" + content: + - type: "text" + text: "Проанализируй этот PDF документ" + - type: "file" + file: + filename: "document.pdf" + file_data: "https://storage.yandexcloud.net/giga-router/181_I-test.pdf" + image_generation: + summary: Генерация изображения + value: + model: "google/gemini-2.5-flash-image-preview" + messages: + - role: "user" + content: "Создай красивое изображение заката над горами" + modalities: ["image", "text"] + audio: + summary: Запрос с аудио файлом + value: + model: "google/gemini-2.5-flash" + messages: + - role: "user" + content: + - type: "text" + text: "Расшифруй этот аудио файл" + - type: "input_audio" + input_audio: + data: "base64-encoded-audio-data" + format: "wav" + web_search: + summary: Запрос с веб-поиском + value: + model: "openai/gpt-4o:online" + messages: + - role: "user" + content: "Какие последние новости о развитии ИИ?" + responses: + '200': + description: Успешный ответ + content: + application/json: + schema: + $ref: '#/components/schemas/ChatCompletionResponse' + examples: + non_streaming: + summary: Обычный ответ + value: + id: "chatcmpl-abc123" + object: "chat.completion" + created: 1677652288 + model: "google/gemini-2.5-flash-lite" + choices: + - index: 0 + message: + role: "assistant" + content: "Привет! У меня всё отлично, спасибо!" + finish_reason: "stop" + usage: + prompt_tokens: 4 + completion_tokens: 147 + total_tokens: 151 + cost: 0.0000592 + prompt_tokens_details: + cached_tokens: 0 + audio_tokens: 0 + cost_details: + upstream_inference_completions_cost: 0.0053 + completion_tokens_details: + reasoning_tokens: 0 + image_tokens: 0 + text/event-stream: + schema: + type: string + description: Server-Sent Events для streaming режима + examples: + streaming: + summary: Streaming ответ + value: | + data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1677652288,"model":"google/gemini-2.5-flash-lite","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]} + + data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1677652288,"model":"google/gemini-2.5-flash-lite","choices":[{"index":0,"delta":{"content":"Привет"},"finish_reason":null}]} + + data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1677652288,"model":"google/gemini-2.5-flash-lite","choices":[{"index":0,"delta":{"content":"!"},"finish_reason":null}]} + + data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1677652288,"model":"google/gemini-2.5-flash-lite","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]} + + data: [DONE] + '400': + $ref: '#/components/responses/ValidationError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '402': + $ref: '#/components/responses/InsufficientBalance' + '500': + $ref: '#/components/responses/InternalError' + + /v1/models: + get: + tags: [ChatModels] + summary: Получить список доступных моделей + description: | + Получить список моделей от популярных провайдеров с ценами в рублях. + **Поддерживаемые провайдеры:** x-ai, deepseek, google, qwen, perplexity, mistralai, openai + + ## Ценообразование + + Все цены возвращаются в рублях: + + responses: + '200': + description: Список моделей успешно получен + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: true + data: + type: object + properties: + models: + type: array + items: + $ref: '#/components/schemas/Model' + total: + type: integer + description: Общее количество моделей + example: 45 + providers: + type: array + items: + type: string + description: Список поддерживаемых провайдеров + example: ["x-ai", "deepseek", "google", "qwen", "perplexity", "mistralai", "openai"] + lastUpdated: + type: string + format: date-time + description: Время последнего обновления + example: "2025-01-28T10:30:00.000Z" + examples: + success: + summary: Успешный ответ + value: + success: true + data: + models: + - id: "google/gemini-2.5-flash-lite-preview-09-2025" + name: "Google: Gemini 2.5 Flash Lite Preview 09-2025" + description: "Gemini 2.5 Flash-Lite is a lightweight reasoning model..." + context_length: 1048576 + pricing_rub: + prompt: 0.000009 + completion: 0.000036 + request: 0 + image: 0 + web_search: 0 + internal_reasoning: 0 + + total: 45 + providers: ["x-ai", "deepseek", "google", "qwen", "perplexity", "mistralai", "openai"] + lastUpdated: "2025-01-28T10:30:00.000Z" + '503': + description: Сервис моделей не готов + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "Models service not ready" + '500': + $ref: '#/components/responses/InternalError' + + /user: + get: + tags: [Authentication] + summary: Получить данные пользователя + description: | + Получает информацию о пользователе, включая баланс, API ключ и данные VK. + + Требует VK подпись в заголовке Authorization. + security: + - VkAuth: [] + responses: + '200': + description: Данные пользователя получены успешно + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: true + data: + type: object + properties: + message: + type: string + example: "User data retrieved successfully!" + vkData: + type: object + description: "Данные от VK" + dbUser: + type: object + properties: + id: + type: string + vkId: + type: string + balance: + type: number + apiKey: + type: string + isActive: + type: boolean + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + timestamp: + type: string + format: date-time + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalError' + + /update-token: + post: + tags: [Authentication] + summary: Обновление API токена + description: | + Генерирует новый API ключ для пользователя. Старый ключ становится недействительным. + + Требует VK авторизацию. + security: + - VkAuth: [] + responses: + '200': + description: Токен успешно обновлен + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: true + data: + type: object + properties: + message: + type: string + example: "API token updated successfully!" + newApiKey: + type: string + description: "Новый API ключ" + example: "sk-abc123..." + user: + type: object + properties: + id: + type: string + vkId: + type: string + balance: + type: number + isActive: + type: boolean + updatedAt: + type: string + format: date-time + timestamp: + type: string + format: date-time + examples: + success: + summary: Успешное обновление токена + value: + success: true + data: + message: "API token updated successfully!" + newApiKey: "sk-abc123def456ghi789jkl012mno345pqr678stu901vwx234yz567890" + user: + id: "user_123" + vkId: "vk_456" + balance: 10.5 + isActive: true + updatedAt: "2024-01-15T10:30:00Z" + timestamp: "2024-01-15T10:30:00Z" + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalError' + +components: + securitySchemes: + ApiKeyBearer: + type: http + scheme: bearer + description: | + API ключ пользователя для доступа к LLM endpoints. + VkAuth: + type: http + scheme: bearer + description: | + VK подпись для авторизации через VK приложение. + Передается в заголовке Authorization как Bearer token. + + schemas: + ChatCompletionRequest: + type: object + required: [messages] + properties: + model: + type: string + default: "google/gemini-2.5-flash-lite" + example: "google/gemini-2.5-flash-lite" + description: | + Модель для генерации ответа. Поддерживаются все модели OpenRouter. + + **Популярные модели:** + - `google/gemini-2.5-flash-lite` - быстрая и дешевая + - `anthropic/claude-3-haiku` - сбалансированная + - `openai/gpt-4o-mini` - OpenAI модель + messages: + type: array + items: + $ref: '#/components/schemas/ChatMessage' + example: + - role: "user" + content: "Привет!" + max_tokens: + type: integer + minimum: 1 + maximum: 4000 + example: 150 + description: Максимальное количество токенов в ответе + temperature: + type: number + minimum: 0 + maximum: 2 + example: 0.7 + description: Креативность ответа (0 = детерминированный, 2 = очень креативный) + top_p: + type: number + minimum: 0 + maximum: 1 + example: 0.9 + description: Nucleus sampling параметр + frequency_penalty: + type: number + minimum: -2 + maximum: 2 + example: 0 + description: Штраф за повторение слов + presence_penalty: + type: number + minimum: -2 + maximum: 2 + example: 0 + description: Штраф за повторение тем + stop: + type: array + items: + type: string + example: ["\n", "###"] + description: Стоп-последовательности для завершения генерации + stream: + type: boolean + default: false + example: true + description: Включить streaming режим (Server-Sent Events) + + ChatMessage: + type: object + required: [role, content] + properties: + role: + type: string + enum: [system, user, assistant] + example: "user" + description: | + Роль отправителя сообщения: + - `system` - системные инструкции для AI + - `user` - сообщения пользователя + - `assistant` - ответы AI + content: + type: string + example: "Привет! Как дела?" + description: Текст сообщения + + ChatCompletionResponse: + type: object + properties: + id: + type: string + example: "chatcmpl-abc123" + object: + type: string + example: "chat.completion" + created: + type: integer + example: 1677652288 + model: + type: string + example: "google/gemini-2.5-flash-lite" + choices: + type: array + items: + $ref: '#/components/schemas/ChatChoice' + usage: + $ref: '#/components/schemas/Usage' + + ChatChoice: + type: object + properties: + index: + type: integer + example: 0 + message: + $ref: '#/components/schemas/ChatMessage' + finish_reason: + type: string + enum: [stop, length, content_filter] + example: "stop" + + Usage: + type: object + description: | + Информация об использовании токенов и стоимости. + + **Структура ответа:** + - `prompt_tokens` - токены запроса + - `completion_tokens` - токены ответа + - `total_tokens` - всего токенов + - `cost` - цена в рублях + properties: + prompt_tokens: + type: integer + example: 4 + description: Количество токенов в запросе + completion_tokens: + type: integer + example: 147 + description: Количество токенов в ответе + total_tokens: + type: integer + example: 151 + description: Общее количество токенов + cost: + type: number + format: float + example: 0.0000592 + description: Стоимость запроса в рублях + + Error: + type: object + properties: + error: + type: string + example: "Invalid API key" + + Model: + type: object + description: Модель LLM с ценами в рублях + properties: + id: + type: string + description: Уникальный идентификатор модели + example: "google/gemini-2.5-flash-lite-preview-09-2025" + canonical_slug: + type: string + description: Канонический слаг модели + example: "google/gemini-2.5-flash-lite-preview-09-2025" + hugging_face_id: + type: string + description: ID модели в Hugging Face + example: "" + name: + type: string + description: Человекочитаемое название модели + example: "Google: Gemini 2.5 Flash Lite Preview 09-2025" + created: + type: integer + description: Unix timestamp создания модели + example: 1758819686 + description: + type: string + description: Описание модели + example: "Gemini 2.5 Flash-Lite is a lightweight reasoning model..." + context_length: + type: integer + description: Максимальная длина контекста + example: 1048576 + architecture: + type: object + description: Архитектура модели + properties: + modality: + type: string + example: "text+image->text" + input_modalities: + type: array + items: + type: string + example: ["file", "image", "text", "audio"] + output_modalities: + type: array + items: + type: string + example: ["text"] + tokenizer: + type: string + example: "Gemini" + instruct_type: + type: string + nullable: true + example: null + pricing_rub: + type: object + description: Цены в рублях + properties: + prompt: + type: number + description: Цена за входной токен + example: 0.000009 + completion: + type: number + description: Цена за выходной токен + example: 0.000036 + request: + type: number + description: Фиксированная цена за запрос + example: 0 + image: + type: number + description: Цена за изображение + example: 0 + web_search: + type: number + description: Цена за веб-поиск + example: 0 + internal_reasoning: + type: number + description: Цена за внутренние рассуждения + example: 0 + top_provider: + type: object + description: Информация о топ-провайдере + properties: + context_length: + type: integer + example: 1048576 + max_completion_tokens: + type: integer + example: 65536 + is_moderated: + type: boolean + example: false + supported_parameters: + type: array + items: + type: string + description: Поддерживаемые параметры + example: ["include_reasoning", "max_tokens", "reasoning", "response_format", "seed", "stop", "structured_outputs", "temperature", "tool_choice", "tools", "top_p"] + default_parameters: + type: object + description: Параметры по умолчанию + properties: + temperature: + type: number + nullable: true + top_p: + type: number + nullable: true + frequency_penalty: + type: number + nullable: true + + FileInfo: + type: object + description: Информация о файле + properties: + id: + type: string + description: Уникальный идентификатор файла + example: "file_123" + name: + type: string + description: Оригинальное имя файла + example: "document.pdf" + type: + type: string + description: MIME-тип файла + example: "application/pdf" + url: + type: string + description: URL для доступа к файлу в облачном хранилище + example: "https://gptutor-bucket.storage.yandexcloud.net/uuid.pdf" + size: + type: integer + description: Размер файла в байтах + example: 1024000 + createdAt: + type: string + format: date-time + description: Дата и время загрузки файла + example: "2024-01-15T10:30:00Z" + + responses: + UnauthorizedError: + description: Ошибка авторизации + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + invalid_api_key: + summary: Неверный API ключ + value: + error: "Invalid API key or inactive user" + missing_api_key: + summary: Отсутствует API ключ + value: + error: "Missing or invalid Authorization header" + + Unauthorized: + description: Ошибка VK авторизации + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + invalid_vk_auth: + summary: Неверная VK авторизация + value: + error: "Invalid VK authorization" + + ValidationError: + description: Ошибка валидации + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + missing_messages: + summary: Отсутствует массив messages + value: + error: "messages array is required" + + InsufficientBalance: + description: Недостаточно средств + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + low_balance: + summary: Недостаточно средств + value: + error: "Insufficient balance" + + InternalError: + description: Внутренняя ошибка сервера + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + server_error: + summary: Ошибка сервера + value: + error: "Internal server error" diff --git a/GPTutor-Backend-v2/docs/package-lock.json b/GPTutor-Backend-v2/docs/package-lock.json new file mode 100644 index 00000000..984d1fdc --- /dev/null +++ b/GPTutor-Backend-v2/docs/package-lock.json @@ -0,0 +1,1212 @@ +{ + "name": "gptutor-internal-docs", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gptutor-internal-docs", + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "express": "^4.18.2" + }, + "devDependencies": { + "nodemon": "^3.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "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/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/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/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "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.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "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/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "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/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/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/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/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-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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/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/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "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.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/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/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "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/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/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/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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/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/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "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.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "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.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "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/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "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/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" + } + } + } +} diff --git a/GPTutor-Backend-v2/docs/package.json b/GPTutor-Backend-v2/docs/package.json new file mode 100644 index 00000000..1ef0c331 --- /dev/null +++ b/GPTutor-Backend-v2/docs/package.json @@ -0,0 +1,44 @@ +{ + "name": "gptutor-internal-docs", + "version": "2.0.0", + "description": "Внутренняя документация API для GPTutor", + "main": "serve-internal.js", + "scripts": { + "start": "node serve-internal.js", + "dev": "nodemon serve-internal.js", + "serve": "node serve-internal.js" + }, + "dependencies": { + "express": "^4.18.2" + }, + "devDependencies": { + "nodemon": "^3.0.1" + }, + "keywords": [ + "gptutor", + "api", + "documentation", + "internal", + "openapi", + "scalar" + ], + "author": "GPTutor Team", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } +} + + + + + + + + + + + + + + diff --git a/GPTutor-Backend-v2/docs/serve-internal.js b/GPTutor-Backend-v2/docs/serve-internal.js new file mode 100644 index 00000000..da3260bb --- /dev/null +++ b/GPTutor-Backend-v2/docs/serve-internal.js @@ -0,0 +1,91 @@ +const express = require("express"); +const path = require("path"); +const fs = require("fs"); + +const app = express(); +const PORT = 3002; + +app.use((req, res, next) => { + console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`); + next(); +}); + +app.use(express.static(path.join(__dirname))); + +app.get("/", (req, res) => { + res.redirect("/internal.html"); +}); + +app.get("/internal-api.yaml", (req, res) => { + const yamlPath = path.join(__dirname, "internal-api.yaml"); + + if (fs.existsSync(yamlPath)) { + res.setHeader("Content-Type", "application/x-yaml"); + res.setHeader("Access-Control-Allow-Origin", "*"); + res.sendFile(yamlPath); + } else { + res.status(404).send("OpenAPI specification not found"); + } +}); + +app.get("/health", (req, res) => { + res.json({ + status: "ok", + service: "GPTutor Internal Documentation Server", + timestamp: new Date().toISOString(), + version: "2.0.0", + }); +}); + +app.use((err, req, res, next) => { + console.error("Error:", err); + res.status(500).json({ + error: "Internal server error", + message: err.message, + }); +}); + +app.use((req, res) => { + res.status(404).json({ + error: "Not found", + message: `Route ${req.method} ${req.url} not found`, + }); +}); + +app.listen(PORT, () => { + console.log("🚀 GPTutor Internal Documentation Server запущен!"); + console.log(`📖 Документация доступна по адресу: http://localhost:${PORT}`); + console.log(`🔧 Health check: http://localhost:${PORT}/health`); + console.log(`📋 OpenAPI spec: http://localhost:${PORT}/internal-api.yaml`); + console.log(""); + console.log("Доступные endpoints:"); + console.log(" GET / - Главная страница"); + console.log(" GET /internal.html - Внутренняя документация"); + console.log(" GET /internal-api.yaml - OpenAPI спецификация"); + console.log(" GET /health - Health check"); + console.log(""); + console.log("Для остановки сервера нажмите Ctrl+C"); +}); + +// Graceful shutdown +process.on("SIGINT", () => { + console.log("\n🛑 Получен сигнал SIGINT, завершение работы..."); + process.exit(0); +}); + +process.on("SIGTERM", () => { + console.log("\n🛑 Получен сигнал SIGTERM, завершение работы..."); + process.exit(0); +}); + + + + + + + + + + + + diff --git a/GPTutor-Backend-v2/docs/serve.js b/GPTutor-Backend-v2/docs/serve.js new file mode 100644 index 00000000..2ad5644b --- /dev/null +++ b/GPTutor-Backend-v2/docs/serve.js @@ -0,0 +1,84 @@ +const http = require("http"); +const fs = require("fs"); +const path = require("path"); + +const port = 8080; +const docsDir = __dirname; + +const mimeTypes = { + ".html": "text/html", + ".yaml": "text/yaml", + ".yml": "text/yaml", + ".css": "text/css", + ".js": "application/javascript", + ".json": "application/json", + ".png": "image/png", + ".jpg": "image/jpeg", + ".gif": "image/gif", + ".svg": "image/svg+xml", + ".ico": "image/x-icon", +}; + +const server = http.createServer((req, res) => { + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader( + "Access-Control-Allow-Methods", + "GET, POST, PUT, DELETE, OPTIONS" + ); + res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); + + if (req.method === "OPTIONS") { + res.writeHead(200); + res.end(); + return; + } + + let filePath = path.join(docsDir, req.url === "/" ? "index.html" : req.url); + + if (!filePath.startsWith(docsDir)) { + res.writeHead(403); + res.end("Forbidden"); + return; + } + + const extname = path.extname(filePath); + const contentType = mimeTypes[extname] || "application/octet-stream"; + + fs.readFile(filePath, (err, content) => { + res.writeHead(200, { "Content-Type": contentType }); + res.end(content, "utf-8"); + }); +}); + +server.listen(port, () => { + console.log("🎉 GPTutor API Documentation Server"); + console.log(`🚀 Server running at http://localhost:${port}`); + console.log(""); + console.log("📋 Available pages:"); + console.log(` 🏠 Welcome page: http://localhost:${port}/`); + console.log(` 📖 API Docs (Light): http://localhost:${port}/index.html`); + console.log(` 🌙 API Docs (Dark): http://localhost:${port}/dark.html`); + console.log( + ` 🧪 Interactive Test: http://localhost:${port}/test-examples.html` + ); + console.log(` 📄 OpenAPI Spec: http://localhost:${port}/openapi.yaml`); + console.log( + ` 📚 Detailed Guide: http://localhost:${port}/CHAT_COMPLETIONS_API.md` + ); + console.log(""); + console.log("✨ Features:"); + console.log(" • Beautiful Scalar UI with light/dark themes"); + console.log(" • Interactive Try-it functionality"); + console.log(" • Comprehensive API documentation"); + console.log(" • Real-time testing interface"); + console.log(""); + console.log("Press Ctrl+C to stop"); +}); + +process.on("SIGINT", () => { + console.log("\n👋 Shutting down documentation server..."); + server.close(() => { + console.log("✅ Server stopped"); + process.exit(0); + }); +}); diff --git a/GPTutor-Backend-v2/init-db.sh b/GPTutor-Backend-v2/init-db.sh new file mode 100644 index 00000000..e1204f0e --- /dev/null +++ b/GPTutor-Backend-v2/init-db.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# Create data directory for database if it doesn't exist +mkdir -p /app/data + +# Check if database exists +if [ ! -f /app/data/prod.db ]; then + echo "Database does not exist, creating and applying migrations..." +fi + +echo "Applying pending migrations..." +# Apply new migrations to existing DB +npx prisma migrate deploy +# Verify migration status +echo "Migration status:" +npx prisma migrate status + +# Start the application +echo "Starting application..." +npm start diff --git a/GPTutor-Backend-v2/logs/.4248ce7dd02d844bae1fc948a46f318a5b61ff90-audit.json b/GPTutor-Backend-v2/logs/.4248ce7dd02d844bae1fc948a46f318a5b61ff90-audit.json new file mode 100644 index 00000000..e2bace96 --- /dev/null +++ b/GPTutor-Backend-v2/logs/.4248ce7dd02d844bae1fc948a46f318a5b61ff90-audit.json @@ -0,0 +1,75 @@ +{ + "keep": { + "days": true, + "amount": 30 + }, + "auditLog": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\.4248ce7dd02d844bae1fc948a46f318a5b61ff90-audit.json", + "files": [ + { + "date": 1759046211927, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\combined-2025-09-28.log", + "hash": "957022e8a64d602bbaea72e6c808f9e7a0f189828ebc753103eb862bc848ff51" + }, + { + "date": 1759130099609, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\combined-2025-09-29.log", + "hash": "6de4ad2625fc994102ead25a806fc9ffa278970c43ffcbfeecda532fc7e365af" + }, + { + "date": 1759255316945, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\combined-2025-09-30.log", + "hash": "26189baf3c82f2f7e784bacc904ed0b09805791dffd1cbd0aeb0fd5497820f9d" + }, + { + "date": 1759267431239, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\combined-2025-10-01.log", + "hash": "d9dcdd217ff8b72c53149277510f9b79e4c46c9fd45e4d3bb58563e1014a7510" + }, + { + "date": 1759354501240, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\combined-2025-10-02.log", + "hash": "e4b7aeba48fa3f1c28345dbb0e2fb2b763f923a54d631e3b9020d62ca567d7e7" + }, + { + "date": 1759441874406, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\combined-2025-10-03.log", + "hash": "11287ab451cafc350326d48ca1612150a6fa71ac8f7594a9ea3f6225b53a3d56" + }, + { + "date": 1759743199962, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\combined-2025-10-06.log", + "hash": "d0e01e8c7f448c7e1b2bf7139581ce6ddb0eb9109be2fe9ee66958f1067c75b9" + }, + { + "date": 1759788413219, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\combined-2025-10-07.log", + "hash": "2e7a555d0acb27dac8cfd9602ca41a4b8aae22a9b3d9ed7a9527fdf485e53867" + }, + { + "date": 1759871884349, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\combined-2025-10-08.log", + "hash": "97b8948ae9bade3f5373f17b85bf5dde9ca8d96b6c6a9b4a3f98facc6cb94003" + }, + { + "date": 1759957213635, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\combined-2025-10-09.log", + "hash": "4a57b8a78db32cd018ff7154901ecff39ace6a49e924ec4d4a70f4a6c2e2cfc4" + }, + { + "date": 1760081523944, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\combined-2025-10-10.log", + "hash": "36b9588218e21c59a7a118155a8ab2f9252e66f2805aff6baf87e6ed2f4d13a0" + }, + { + "date": 1760278745765, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\combined-2025-10-12.log", + "hash": "2d0ca52bf4956a0f6f927d5e604a6c9b0888424e2859c24ffccd6c998ff6a365" + }, + { + "date": 1760526675188, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\combined-2025-10-15.log", + "hash": "3ed30181c0b1c4fb9e85f45b41472f1afc53e9751a18e392230785f41bc0d889" + } + ], + "hashType": "sha256" +} \ No newline at end of file diff --git a/GPTutor-Backend-v2/logs/.700b7064ec9c761f8ae8fa6770e8cb354853e7f4-audit.json b/GPTutor-Backend-v2/logs/.700b7064ec9c761f8ae8fa6770e8cb354853e7f4-audit.json new file mode 100644 index 00000000..ed02a43b --- /dev/null +++ b/GPTutor-Backend-v2/logs/.700b7064ec9c761f8ae8fa6770e8cb354853e7f4-audit.json @@ -0,0 +1,20 @@ +{ + "keep": { + "days": true, + "amount": 30 + }, + "auditLog": "G:\\project\\GPTutor\\GPTutor-Backend-v2\\logs\\.700b7064ec9c761f8ae8fa6770e8cb354853e7f4-audit.json", + "files": [ + { + "date": 1759226270953, + "name": "G:\\project\\GPTutor\\GPTutor-Backend-v2\\logs\\combined-2025-09-30.log", + "hash": "f04015f68811614c2681199a5928779ca750ee97a930ed6d4f13204265410a94" + }, + { + "date": 1759307305692, + "name": "G:\\project\\GPTutor\\GPTutor-Backend-v2\\logs\\combined-2025-10-01.log", + "hash": "df4e3707b7760db35fecfa2eacd8dca769f7853f831cf505686019fadaa1de38" + } + ], + "hashType": "sha256" +} \ No newline at end of file diff --git a/GPTutor-Backend-v2/logs/.7bd753b0e1ca4905c3b8c68319bdeb985d41567d-audit.json b/GPTutor-Backend-v2/logs/.7bd753b0e1ca4905c3b8c68319bdeb985d41567d-audit.json new file mode 100644 index 00000000..dc2edca5 --- /dev/null +++ b/GPTutor-Backend-v2/logs/.7bd753b0e1ca4905c3b8c68319bdeb985d41567d-audit.json @@ -0,0 +1,20 @@ +{ + "keep": { + "days": true, + "amount": 30 + }, + "auditLog": "G:\\project\\GPTutor\\GPTutor-Backend-v2\\logs\\.7bd753b0e1ca4905c3b8c68319bdeb985d41567d-audit.json", + "files": [ + { + "date": 1759226270955, + "name": "G:\\project\\GPTutor\\GPTutor-Backend-v2\\logs\\api-2025-09-30.log", + "hash": "c2dbf31bf19a2b6910b4967209fe721bed982f3fb4dc3b0d69e6b63ae32dca7b" + }, + { + "date": 1759307305694, + "name": "G:\\project\\GPTutor\\GPTutor-Backend-v2\\logs\\api-2025-10-01.log", + "hash": "9ce71017dedf060da5dfa3a6404bc78357241be4e3794dbc93164d1e3d0cfeda" + } + ], + "hashType": "sha256" +} \ No newline at end of file diff --git a/GPTutor-Backend-v2/logs/.a74aac600712d8a095c9114e3ee577ef47d166ea-audit.json b/GPTutor-Backend-v2/logs/.a74aac600712d8a095c9114e3ee577ef47d166ea-audit.json new file mode 100644 index 00000000..d7676f1f --- /dev/null +++ b/GPTutor-Backend-v2/logs/.a74aac600712d8a095c9114e3ee577ef47d166ea-audit.json @@ -0,0 +1,50 @@ +{ + "keep": { + "days": true, + "amount": 14 + }, + "auditLog": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\.a74aac600712d8a095c9114e3ee577ef47d166ea-audit.json", + "files": [ + { + "date": 1759354501238, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\error-2025-10-02.log", + "hash": "3405c5f8ad5db499e61b2e15e2b37ee8f9a705eb56f1d76f0ca0063203532a0e" + }, + { + "date": 1759441874397, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\error-2025-10-03.log", + "hash": "969a040ad764ba8ca6d885d42697f011d61d696ecdb1a7a0c98dbbf76c8abc66" + }, + { + "date": 1759743199954, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\error-2025-10-06.log", + "hash": "7644aa3737d2fccc3b39a83ca95d8b1962d8cf86b1a997f9165f7bbddb64c900" + }, + { + "date": 1759855595770, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\error-2025-10-07.log", + "hash": "d87aec1e04cae1ee8b66eeb5a91d08caef76e84f9f32e109c3b72e3be73856c7" + }, + { + "date": 1759910130771, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\error-2025-10-08.log", + "hash": "f0112612f00e21f741257ecd539da5746fd61f3181ba1c7642c4385fe3715dbb" + }, + { + "date": 1759957217847, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\error-2025-10-09.log", + "hash": "4acef1aa1e22e96ab3aa03c67c791ac76f991221480eb647581b701e659854d6" + }, + { + "date": 1760278745761, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\error-2025-10-12.log", + "hash": "75047cbb1887023fe63d092a7d4df08f0d28fe5bd7923cbc31b16f3d2f269966" + }, + { + "date": 1760526675178, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\error-2025-10-15.log", + "hash": "cf3368b7109396531516f4fb4eb4e6174357df9cfe98d8da52f81b4957c0d775" + } + ], + "hashType": "sha256" +} \ No newline at end of file diff --git a/GPTutor-Backend-v2/logs/.bb75621182600988c7473c833dc9cc16576776f8-audit.json b/GPTutor-Backend-v2/logs/.bb75621182600988c7473c833dc9cc16576776f8-audit.json new file mode 100644 index 00000000..3b71bb4c --- /dev/null +++ b/GPTutor-Backend-v2/logs/.bb75621182600988c7473c833dc9cc16576776f8-audit.json @@ -0,0 +1,20 @@ +{ + "keep": { + "days": true, + "amount": 14 + }, + "auditLog": "G:\\project\\GPTutor\\GPTutor-Backend-v2\\logs\\.bb75621182600988c7473c833dc9cc16576776f8-audit.json", + "files": [ + { + "date": 1759226270951, + "name": "G:\\project\\GPTutor\\GPTutor-Backend-v2\\logs\\error-2025-09-30.log", + "hash": "3199b1428544ccc2a696eab3d225f1c17866c130a4ed6f48f356fd7cde1e39fb" + }, + { + "date": 1759307305690, + "name": "G:\\project\\GPTutor\\GPTutor-Backend-v2\\logs\\error-2025-10-01.log", + "hash": "b6ef795f6af1e3b5eb697a91ced135f259dbc2fa761968ea1317fa23ea7bf2d7" + } + ], + "hashType": "sha256" +} \ No newline at end of file diff --git a/GPTutor-Backend-v2/logs/.dae23bed5130afafe72e37b81cf5209f3ef8bbaf-audit.json b/GPTutor-Backend-v2/logs/.dae23bed5130afafe72e37b81cf5209f3ef8bbaf-audit.json new file mode 100644 index 00000000..5533277e --- /dev/null +++ b/GPTutor-Backend-v2/logs/.dae23bed5130afafe72e37b81cf5209f3ef8bbaf-audit.json @@ -0,0 +1,75 @@ +{ + "keep": { + "days": true, + "amount": 30 + }, + "auditLog": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\.dae23bed5130afafe72e37b81cf5209f3ef8bbaf-audit.json", + "files": [ + { + "date": 1759046211929, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\api-2025-09-28.log", + "hash": "7e3e02ec325de51199240903431f044c996d4e2379ab4b43b9e8909956f90964" + }, + { + "date": 1759130099610, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\api-2025-09-29.log", + "hash": "e2ec2102e6b7e97ca321b5226d23ed50acd7fb860920057367bcbf631b2ae71d" + }, + { + "date": 1759255316948, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\api-2025-09-30.log", + "hash": "3c155086fd96f86b7f2cd1f95097a02a1f8236ed857af5587f9468ac42788b8a" + }, + { + "date": 1759267431253, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\api-2025-10-01.log", + "hash": "5e6807da4f4c269a84961333be78591c0ecfc0c510d2768ae1a287f378c01a44" + }, + { + "date": 1759354501242, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\api-2025-10-02.log", + "hash": "801a635c11c69aa58959e3f04dccb9893e00227cee8c592012129395f3fee04c" + }, + { + "date": 1759441874413, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\api-2025-10-03.log", + "hash": "080d597be87c8762e2666a4c9aa935bbad7b241d13bd4d9c64588215e3503385" + }, + { + "date": 1759743199970, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\api-2025-10-06.log", + "hash": "e17223482775e67f8286ec8d4ed3e855eeb9b2ecd2a77dff92b35ca4299778ae" + }, + { + "date": 1759788413236, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\api-2025-10-07.log", + "hash": "b5705e649a58d3222809b366de10b5af2f559115b1b6c8556d7fa0dba52bbb22" + }, + { + "date": 1759871884355, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\api-2025-10-08.log", + "hash": "e11ad8b89c6cef6610c83dcc6114481813695397be5f5c2e8609758d223c4c11" + }, + { + "date": 1759957213637, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\api-2025-10-09.log", + "hash": "2660501b676cd30d28f561453542c1cb9f629a7ddc45383f22078dce2b283727" + }, + { + "date": 1760081524035, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\api-2025-10-10.log", + "hash": "388c523cb472abdefa7fcc9f30c3180837e78dc9051acb2f5abcb49b97934aff" + }, + { + "date": 1760278745767, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\api-2025-10-12.log", + "hash": "44f86b8ada9f3157bedc11f649c348091232af001a112cd7075392d54d857d73" + }, + { + "date": 1760526675195, + "name": "C:\\Users\\grisha_blyat\\PhpstormProjects\\untitled10\\GPTutor\\GPTutor-Backend-v2\\logs\\api-2025-10-15.log", + "hash": "e1994c77e535e65eab3cf015888f2c35b5e2b21c7b2a8cb3d65911c252f89b1d" + } + ], + "hashType": "sha256" +} \ No newline at end of file diff --git a/GPTutor-Backend-v2/logs/.gitignore b/GPTutor-Backend-v2/logs/.gitignore new file mode 100644 index 00000000..84348f46 --- /dev/null +++ b/GPTutor-Backend-v2/logs/.gitignore @@ -0,0 +1,9 @@ +# Ignore all log files +*.log +*.log.* + +# Keep the directory +!.gitignore + + + diff --git a/GPTutor-Backend-v2/nodemon.json b/GPTutor-Backend-v2/nodemon.json new file mode 100644 index 00000000..a883ee5f --- /dev/null +++ b/GPTutor-Backend-v2/nodemon.json @@ -0,0 +1,7 @@ +{ + "watch": ["src"], + "ext": "ts,json", + "ignore": ["src/**/*.spec.ts"], + "exec": "ts-node src/index.ts" +} + diff --git a/GPTutor-Backend-v2/package-lock.json b/GPTutor-Backend-v2/package-lock.json new file mode 100644 index 00000000..e2150419 --- /dev/null +++ b/GPTutor-Backend-v2/package-lock.json @@ -0,0 +1,4248 @@ +{ + "name": "gptutor-backend-v2", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gptutor-backend-v2", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@a2seven/yoo-checkout": "^1.1.4", + "@fastify/cors": "^10.0.1", + "@fastify/helmet": "^12.0.1", + "@fastify/jwt": "^9.0.1", + "@fastify/multipart": "^9.0.1", + "@fastify/rate-limit": "^10.1.1", + "@fastify/static": "^8.0.1", + "@fastify/swagger": "^9.1.0", + "@fastify/swagger-ui": "^5.0.1", + "@prisma/client": "^6.16.2", + "@types/uuid": "^10.0.0", + "@types/winston": "^2.4.4", + "aws-sdk": "^2.1691.0", + "compress-pdf": "0.5.5", + "crypto-js": "^4.2.0", + "easy-yandex-s3": "2.0.0", + "fastify": "^5.1.0", + "libreoffice-convert": "^1.7.0", + "openai": "^5.23.1", + "sharp": "^0.33.5", + "uuid": "^13.0.0", + "winston": "^3.17.0", + "winston-daily-rotate-file": "^5.0.0" + }, + "devDependencies": { + "@types/crypto-js": "^4.2.2", + "@types/node": "^22.10.5", + "node-fetch": "^3.3.2", + "nodemon": "^3.1.9", + "prisma": "^6.16.2", + "ts-node": "^10.9.2", + "typescript": "^5.7.3" + } + }, + "node_modules/@a2seven/yoo-checkout": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@a2seven/yoo-checkout/-/yoo-checkout-1.1.4.tgz", + "integrity": "sha512-Xn8E6fJKVUpSqTTEgigoi0Woyvmc1/UuHukwpQUTEtxD2l4skTEeqPlhd0J4xum6lQ/0eGzeJP+/MgpkE9Ak9g==", + "license": "MIT", + "dependencies": { + "@types/axios": "^0.14.0", + "axios": "^0.21.1", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@a2seven/yoo-checkout/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fastify/accept-negotiator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.0.1.tgz", + "integrity": "sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.2.tgz", + "integrity": "sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz", + "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==", + "license": "MIT" + }, + "node_modules/@fastify/cors": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-10.1.0.tgz", + "integrity": "sha512-MZyBCBJtII60CU9Xme/iE4aEy8G7QpzGR8zkdXZkDFt7ElEMachbE61tfhAG/bvSaULlqlf0huMT12T7iqEmdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "mnemonist": "0.40.0" + } + }, + "node_modules/@fastify/deepmerge": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-3.1.0.tgz", + "integrity": "sha512-lCVONBQINyNhM6LLezB6+2afusgEYR4G8xenMsfe+AT+iZ7Ca6upM5Ha8UkZuYSnuMw3GWl/BiPXnLMi/gSxuQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz", + "integrity": "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^6.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.0.tgz", + "integrity": "sha512-kJExsp4JCms7ipzg7SJ3y8DwmePaELHxKYtg+tZow+k0znUTf3cb+npgyqm8+ATZOdmfgfydIebPDWM172wfyA==", + "license": "MIT" + }, + "node_modules/@fastify/helmet": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@fastify/helmet/-/helmet-12.0.1.tgz", + "integrity": "sha512-kkjBcedWwdflRThovGuvN9jB2QQLytBqArCFPdMIb7o2Fp0l/H3xxYi/6x/SSRuH/FFt9qpTGIfJz2bfnMrLqA==", + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "helmet": "^7.1.0" + } + }, + "node_modules/@fastify/jwt": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@fastify/jwt/-/jwt-9.1.0.tgz", + "integrity": "sha512-CiGHCnS5cPMdb004c70sUWhQTfzrJHAeTywt7nVw6dAiI0z1o4WRvU94xfijhkaId4bIxTCOjFgn4sU+Gvk43w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "@lukeed/ms": "^2.0.2", + "fast-jwt": "^5.0.0", + "fastify-plugin": "^5.0.0", + "steed": "^1.1.3" + } + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/multipart": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@fastify/multipart/-/multipart-9.2.1.tgz", + "integrity": "sha512-U4221XDMfzCUtfzsyV1/PkR4MNgKI0158vUUyn/oF2Tl6RxMc+N7XYLr5fZXQiEC+Fmw5zFaTjxsTGTgtDtK+g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^3.0.0", + "@fastify/deepmerge": "^3.0.0", + "@fastify/error": "^4.0.0", + "fastify-plugin": "^5.0.0", + "secure-json-parse": "^4.0.0" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.0.0.tgz", + "integrity": "sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA==", + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@fastify/rate-limit": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@fastify/rate-limit/-/rate-limit-10.3.0.tgz", + "integrity": "sha512-eIGkG9XKQs0nyynatApA3EVrojHOuq4l6fhB4eeCk4PIOeadvOJz9/4w3vGI44Go17uaXOWEcPkaD8kuKm7g6Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "fastify-plugin": "^5.0.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/@fastify/send": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@fastify/send/-/send-4.1.0.tgz", + "integrity": "sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "escape-html": "~1.0.3", + "fast-decode-uri-component": "^1.0.1", + "http-errors": "^2.0.0", + "mime": "^3" + } + }, + "node_modules/@fastify/static": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-8.2.0.tgz", + "integrity": "sha512-PejC/DtT7p1yo3p+W7LiUtLMsV8fEvxAK15sozHy9t8kwo5r0uLYmhV/inURmGz1SkHZFz/8CNtHLPyhKcx4SQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/accept-negotiator": "^2.0.0", + "@fastify/send": "^4.0.0", + "content-disposition": "^0.5.4", + "fastify-plugin": "^5.0.0", + "fastq": "^1.17.1", + "glob": "^11.0.0" + } + }, + "node_modules/@fastify/swagger": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/@fastify/swagger/-/swagger-9.5.2.tgz", + "integrity": "sha512-8e8w/LItg/cF6IR/hYKtnt+E0QImees5o3YWJsTLxaIk+tzNUEc6Z2Ursi4oOHWwUlKjUCnV6yh5z5ZdxvlsWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "json-schema-resolver": "^3.0.0", + "openapi-types": "^12.1.3", + "rfdc": "^1.3.1", + "yaml": "^2.4.2" + } + }, + "node_modules/@fastify/swagger-ui": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@fastify/swagger-ui/-/swagger-ui-5.2.3.tgz", + "integrity": "sha512-e7ivEJi9EpFcxTONqICx4llbpB2jmlI+LI1NQ/mR7QGQnyDOqZybPK572zJtcdHZW4YyYTBHcP3a03f1pOh0SA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/static": "^8.0.0", + "fastify-plugin": "^5.0.0", + "openapi-types": "^12.1.3", + "rfdc": "^1.3.1", + "yaml": "^2.4.1" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@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", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@prisma/client": { + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.16.2.tgz", + "integrity": "sha512-E00PxBcalMfYO/TWnXobBVUai6eW/g5OsifWQsQDzJYm7yaY+IRLo7ZLsaefi0QkTpxfuhFcQ/w180i6kX3iJw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.16.2.tgz", + "integrity": "sha512-mKXSUrcqXj0LXWPmJsK2s3p9PN+aoAbyMx7m5E1v1FufofR1ZpPoIArjjzOIm+bJRLLvYftoNYLx1tbHgF9/yg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.16.12", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.16.2.tgz", + "integrity": "sha512-bo4/gA/HVV6u8YK2uY6glhNsJ7r+k/i5iQ9ny/3q5bt9ijCj7WMPUwfTKPvtEgLP+/r26Z686ly11hhcLiQ8zA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.16.2.tgz", + "integrity": "sha512-7yf3AjfPUgsg/l7JSu1iEhsmZZ/YE00yURPjTikqm2z4btM0bCl2coFtTGfeSOWbQMmq45Jab+53yGUIAT1sjA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.16.2", + "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "@prisma/fetch-engine": "6.16.2", + "@prisma/get-platform": "6.16.2" + } + }, + "node_modules/@prisma/engines-version": { + "version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43.tgz", + "integrity": "sha512-ThvlDaKIVrnrv97ujNFDYiQbeMQpLa0O86HFA2mNoip4mtFqM7U5GSz2ie1i2xByZtvPztJlNRgPsXGeM/kqAA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.16.2.tgz", + "integrity": "sha512-wPnZ8DMRqpgzye758ZvfAMiNJRuYpz+rhgEBZi60ZqDIgOU2694oJxiuu3GKFeYeR/hXxso4/2oBC243t/whxQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.16.2", + "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "@prisma/get-platform": "6.16.2" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.16.2.tgz", + "integrity": "sha512-U/P36Uke5wS7r1+omtAgJpEB94tlT4SdlgaeTc6HVTTT93pXj7zZ+B/cZnmnvjcNPfWddgoDx8RLjmQwqGDYyA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.16.2" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/axios": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.4.tgz", + "integrity": "sha512-9JgOaunvQdsQ/qW2OPmE5+hCeUB52lQSolecrFrthct55QekhmXEwT203s20RL+UHtCQc15y3VXpby9E7Kkh/g==", + "deprecated": "This is a stub types definition. axios provides its own type definitions, so you do not need this installed.", + "license": "MIT", + "dependencies": { + "axios": "*" + } + }, + "node_modules/@types/crypto-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", + "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.6.tgz", + "integrity": "sha512-r8uszLPpeIWbNKtvWRt/DbVi5zbqZyj1PTmhRMqBMvDnaz1QpmSKujUtJLrqGZeoM8v72MfYggDceY4K1itzWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, + "node_modules/@types/winston": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/winston/-/winston-2.4.4.tgz", + "integrity": "sha512-BVGCztsypW8EYwJ+Hq+QNYiT/MUyCif0ouBH+flrY66O5W+KIXAMML6E/0fJpm7VjIzgangahl5S03bJJQGrZw==", + "deprecated": "This is a stub types definition. winston provides its own type definitions, so you do not need this installed.", + "license": "MIT", + "dependencies": { + "winston": "*" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/async-generator-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-generator-function/-/async-generator-function-1.0.0.tgz", + "integrity": "sha512-+NAXNqgCrB95ya4Sr66i1CL2hqLVckAk7xwRYWdcm39/ELQ6YNn1aw5r0bdQtqNZgQpEWzc5yc/igXc7aL5SLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/avvio": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.1.0.tgz", + "integrity": "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==", + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/aws-sdk": { + "version": "2.1692.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1692.0.tgz", + "integrity": "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/c12/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/compress-pdf": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/compress-pdf/-/compress-pdf-0.5.5.tgz", + "integrity": "sha512-f/m+hWv00EWJSjzNYE6wPEppUiKNwxr36EYce+7J7V4TqDEgsNRQGF6ZZjSOFyMF9GnTdrWF23uswgi/cQUjJw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "dotenv": "17.2.1", + "lodash": "4.17.21" + }, + "bin": { + "compress-pdf": "bin/compress-pdf" + }, + "engines": { + "node": ">=20.0.0", + "yarn": ">= 1.22.0" + } + }, + "node_modules/compress-pdf/node_modules/dotenv": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", + "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "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/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz", + "integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "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/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/easy-yandex-s3": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/easy-yandex-s3/-/easy-yandex-s3-2.0.0.tgz", + "integrity": "sha512-19iLtO3pHhclFhYrhFanIIKgTAAYBpikXR5lcNbA/JhHJLaN4vwN6icwHSRYaXSdMDNK0MXkSD3s/MqJ25IUhw==", + "license": "ISC", + "dependencies": { + "aws-sdk": "^2.1231.0", + "file-type": "^12.0.1", + "mime-types": "^2.1.24", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/easy-yandex-s3/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/effect": { + "version": "3.16.12", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.16.12.tgz", + "integrity": "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "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-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "license": "MIT", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.0.1.tgz", + "integrity": "sha512-s7SJE83QKBZwg54dIbD5rCtzOBVD43V1ReWXXYqBgwCwHLYAAT0RQc/FmrQglXqWPpz6omtryJQOau5jI4Nrvg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0", + "json-schema-ref-resolver": "^2.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-jwt": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/fast-jwt/-/fast-jwt-5.0.6.tgz", + "integrity": "sha512-LPE7OCGUl11q3ZgW681cEU2d0d2JZ37hhJAmetCgNyW8waVaJVZXhyFF6U2so1Iim58Yc7pfxJe2P7MNetQH2g==", + "license": "Apache-2.0", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "asn1.js": "^5.4.1", + "ecdsa-sig-formatter": "^1.0.11", + "mnemonist": "^0.40.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastfall": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/fastfall/-/fastfall-1.5.1.tgz", + "integrity": "sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==", + "license": "MIT", + "dependencies": { + "reusify": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fastify": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.6.1.tgz", + "integrity": "sha512-WjjlOciBF0K8pDUPZoGPhqhKrQJ02I8DKaDIfO51EL0kbSMwQFl85cRwhOvmSDWoukNOdTo27gLN549pLCcH7Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.0", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^6.0.0", + "find-my-way": "^9.0.0", + "light-my-request": "^6.0.0", + "pino": "^9.0.0", + "process-warning": "^5.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastify-plugin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.0.1.tgz", + "integrity": "sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==", + "license": "MIT" + }, + "node_modules/fastparallel": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/fastparallel/-/fastparallel-2.4.1.tgz", + "integrity": "sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4", + "xtend": "^4.0.2" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fastseries": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/fastseries/-/fastseries-1.7.2.tgz", + "integrity": "sha512-dTPFrPGS8SNSzAt7u/CbMKCJ3s01N04s4JFbORHcmyvVfVKmbhMD1VtRbh5enGHxkaQDqWyLefiKOGGmohGDDQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-stream-rotator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz", + "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.1" + } + }, + "node_modules/file-type": { + "version": "12.4.2", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-12.4.2.tgz", + "integrity": "sha512-UssQP5ZgIOKelfsaB5CuGAL+Y+q7EmONuiwF3N5HAH0t27rvrttgi6Ra9k/+DVaY9UF6+ybxu5pOXLUdA8N7Vg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-my-way": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.3.0.tgz", + "integrity": "sha512-eRoFWQw+Yv2tuYlK2pjFS2jGXSxSppAs3hSQjfxVKxM5amECzIgYYc1FEI8ZmhSh/Ig+FrKEz43NLRKJjYCZVg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.0.tgz", + "integrity": "sha512-xPypGGincdfyl/AiSGa7GjXLkvld9V7GjZlowup9SHIJnQnHLFiLODCd/DqKOp0PBagbHJ68r1KJI9Mut7m4sA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.1.tgz", + "integrity": "sha512-fk1ZVEeOX9hVZ6QzoBNEC55+Ucqg4sTVwrVuigZhuRPESVFpMyXnd3sbXvPOwp7Y9riVyANiqhEuRF0G1aVSeQ==", + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "async-generator-function": "^1.0.0", + "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", + "generator-function": "^2.0.0", + "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/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/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/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", + "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "license": "BSD-3-Clause" + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "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/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jiti": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.0.tgz", + "integrity": "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-2.0.1.tgz", + "integrity": "sha512-HG0SIB9X4J8bwbxCbnd5FfPEbcXAJYTi1pBJeP/QPON+w8ovSME8iRG+ElHNxZNX2Qh6eYn1GdzJFS4cDFfx0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-resolver/-/json-schema-resolver-3.0.0.tgz", + "integrity": "sha512-HqMnbz0tz2DaEJ3ntsqtx3ezzZyDE7G56A/pPY/NGmrPu76UzsWquOpHFRAf5beTNXoH2LU5cQePVvRli1nchA==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fast-uri": "^3.0.5", + "rfdc": "^1.1.4" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/Eomm/json-schema-resolver?sponsor=1" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/libreoffice-convert": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/libreoffice-convert/-/libreoffice-convert-1.7.0.tgz", + "integrity": "sha512-sPG+ikVfJGNf6zEZxvmfNSAQQ+wM78Zdg2C1ikAiqL3NlmPTs3004TypsxICJ6IGkrqXr9wlxBs6wB905vYLOw==", + "license": "MIT", + "dependencies": { + "async": "^3.2.3", + "tmp": "^0.2.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/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/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "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/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "license": "ISC", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mnemonist": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.0.tgz", + "integrity": "sha512-kdd8AFNig2AD5Rkih7EPCXhu/iMvwevQFX/uEiGhZyPZi7fHqOoF4V4kHLpCfysxXMgQ4B52kdPMCwARshKvEg==", + "license": "MIT", + "dependencies": { + "obliterator": "^2.0.4" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nypm": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", + "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.2", + "pathe": "^2.0.3", + "pkg-types": "^2.3.0", + "tinyexec": "^1.0.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/obliterator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", + "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", + "license": "MIT" + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/openai": { + "version": "5.23.1", + "resolved": "https://registry.npmjs.org/openai/-/openai-5.23.1.tgz", + "integrity": "sha512-APxMtm5mln4jhKhAr0d5zP9lNsClx4QwJtg8RUvYSSyxYCTHLNJnLEcSHbJ6t0ori8Pbr9HZGfcPJ7LEy73rvQ==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT" + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.11.0.tgz", + "integrity": "sha512-+YIodBB9sxcWeR8PrXC2K3gEDyfkUuVEITOcbqrfcj+z5QW4ioIcqZfYFbrLTYLsmAwunbS7nfU/dpBB6PZc1g==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "license": "MIT" + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prisma": { + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.16.2.tgz", + "integrity": "sha512-aRvldGE5UUJTtVmFiH3WfNFNiqFlAtePUxcI0UEGlnXCX7DqhiMT5TRYwncHFeA/Reca5W6ToXXyCMTeFPdSXA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.16.2", + "@prisma/engines": "6.16.2" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", + "license": "MIT" + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "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-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex2": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.0.0.tgz", + "integrity": "sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", + "license": "ISC" + }, + "node_modules/secure-json-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.0.0.tgz", + "integrity": "sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/sharp/node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/steed": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/steed/-/steed-1.1.3.tgz", + "integrity": "sha512-EUkci0FAUiE4IvGTSKcDJIQ/eRUP2JJb56+fvZ4sdnguLTqIdKjSxUe138poW8mkvKWXW2sFPrgTsxqoISnmoA==", + "license": "MIT", + "dependencies": { + "fastfall": "^1.5.0", + "fastparallel": "^2.2.0", + "fastq": "^1.3.0", + "fastseries": "^1.7.0", + "reusify": "^1.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/tinyexec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "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/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "license": "MIT", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-daily-rotate-file": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz", + "integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==", + "license": "MIT", + "dependencies": { + "file-stream-rotator": "^0.6.1", + "object-hash": "^3.0.0", + "triple-beam": "^1.4.1", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "winston": "^3" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/GPTutor-Backend-v2/package.json b/GPTutor-Backend-v2/package.json new file mode 100644 index 00000000..c9100c7e --- /dev/null +++ b/GPTutor-Backend-v2/package.json @@ -0,0 +1,62 @@ +{ + "name": "gptutor-backend-v2", + "version": "1.0.0", + "description": "GPTutor Backend v2 - Fastify + SQLite + Prisma", + "main": "dist/index.js", + "scripts": { + "dev": "nodemon src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "test": "node test.js", + "test:rate-limits": "node scripts/test-rate-limits.js", + "test:rate-limit-middleware": "npx ts-node src/middleware/rateLimitMiddleware.test.ts", + "db:generate": "prisma generate", + "db:push": "prisma db push", + "db:migrate": "npx prisma migrate dev", + "db:studio": "prisma studio", + "docs": "node docs/serve.js", + "docs:open": "node docs/serve.js & start http://localhost:8080" + }, + "keywords": [ + "fastify", + "prisma", + "sqlite", + "typescript" + ], + "author": "", + "license": "ISC", + "dependencies": { + "@a2seven/yoo-checkout": "^1.1.4", + "@fastify/cors": "^10.0.1", + "@fastify/helmet": "^12.0.1", + "@fastify/jwt": "^9.0.1", + "@fastify/multipart": "^9.0.1", + "@fastify/rate-limit": "^10.1.1", + "@fastify/static": "^8.0.1", + "@fastify/swagger": "^9.1.0", + "@fastify/swagger-ui": "^5.0.1", + "@prisma/client": "^6.16.2", + "@types/uuid": "^10.0.0", + "@types/winston": "^2.4.4", + "aws-sdk": "^2.1691.0", + "compress-pdf": "0.5.5", + "crypto-js": "^4.2.0", + "easy-yandex-s3": "2.0.0", + "fastify": "^5.1.0", + "libreoffice-convert": "^1.7.0", + "openai": "^5.23.1", + "sharp": "^0.33.5", + "uuid": "^13.0.0", + "winston": "^3.17.0", + "winston-daily-rotate-file": "^5.0.0" + }, + "devDependencies": { + "@types/crypto-js": "^4.2.2", + "@types/node": "^22.10.5", + "node-fetch": "^3.3.2", + "nodemon": "^3.1.9", + "prisma": "^6.16.2", + "ts-node": "^10.9.2", + "typescript": "^5.7.3" + } +} diff --git a/GPTutor-Backend-v2/prisma/migrations/20251009103248_init/migration.sql b/GPTutor-Backend-v2/prisma/migrations/20251009103248_init/migration.sql new file mode 100644 index 00000000..8dde0039 --- /dev/null +++ b/GPTutor-Backend-v2/prisma/migrations/20251009103248_init/migration.sql @@ -0,0 +1,43 @@ +-- CreateTable +CREATE TABLE "users" ( + "id" TEXT NOT NULL PRIMARY KEY, + "email" TEXT, + "username" TEXT, + "vkId" TEXT, + "balance" REAL NOT NULL DEFAULT 50.0, + "apiKey" TEXT NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- CreateTable +CREATE TABLE "files" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "name" TEXT NOT NULL, + "url" TEXT NOT NULL, + "size" INTEGER NOT NULL, + "notStatic" BOOLEAN NOT NULL DEFAULT true, + "originalName" TEXT, + "originalSize" INTEGER, + "converted" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "files_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "users_username_key" ON "users"("username"); + +-- CreateIndex +CREATE UNIQUE INDEX "users_vkId_key" ON "users"("vkId"); + +-- CreateIndex +CREATE UNIQUE INDEX "users_apiKey_key" ON "users"("apiKey"); + + diff --git a/GPTutor-Backend-v2/prisma/migrations/20251012150951_add_payments_table/migration.sql b/GPTutor-Backend-v2/prisma/migrations/20251012150951_add_payments_table/migration.sql new file mode 100644 index 00000000..70b93c86 --- /dev/null +++ b/GPTutor-Backend-v2/prisma/migrations/20251012150951_add_payments_table/migration.sql @@ -0,0 +1,17 @@ +-- CreateTable +CREATE TABLE "payments" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL, + "yookassaId" TEXT NOT NULL, + "amount" REAL NOT NULL, + "currency" TEXT NOT NULL DEFAULT 'RUB', + "status" TEXT NOT NULL DEFAULT 'pending', + "description" TEXT, + "confirmationUrl" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "payments_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "payments_yookassaId_key" ON "payments"("yookassaId"); diff --git a/GPTutor-Backend-v2/prisma/migrations/migration_lock.toml b/GPTutor-Backend-v2/prisma/migrations/migration_lock.toml new file mode 100644 index 00000000..2a5a4441 --- /dev/null +++ b/GPTutor-Backend-v2/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "sqlite" diff --git a/GPTutor-Backend-v2/prisma/schema.prisma b/GPTutor-Backend-v2/prisma/schema.prisma new file mode 100644 index 00000000..84aa4663 --- /dev/null +++ b/GPTutor-Backend-v2/prisma/schema.prisma @@ -0,0 +1,69 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +model User { + id String @id @default(cuid()) + email String? @unique + username String? @unique + vkId String? @unique + + balance Float @default(50.0) + apiKey String @unique + isActive Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + files File[] + payments Payment[] + + @@map("users") +} + +model File { + id String @id @default(cuid()) + userId String + type String + name String + url String + size Int + notStatic Boolean @default(true) + + originalName String? + originalSize Int? + converted Boolean @default(false) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map("files") +} + +model Payment { + id String @id @default(cuid()) + userId String + yookassaId String @unique + amount Float + currency String @default("RUB") + status String @default("pending") // pending, succeeded, canceled + description String? + confirmationUrl String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map("payments") +} diff --git a/GPTutor-Backend-v2/src/config/rateLimitConfig.ts b/GPTutor-Backend-v2/src/config/rateLimitConfig.ts new file mode 100644 index 00000000..f9992f3d --- /dev/null +++ b/GPTutor-Backend-v2/src/config/rateLimitConfig.ts @@ -0,0 +1,57 @@ +import { RateLimitOptions } from "../middleware/rateLimitMiddleware"; + +export const RATE_LIMIT_CONFIG: RateLimitOptions = { + "/health": { + max: 100, + timeWindow: 60 * 1000, + }, + + "/user": { + max: 30, + timeWindow: 60 * 1000, + }, + + "/update-token": { + max: 5, + timeWindow: 60 * 1000, + }, + + "/upload": { + max: 10, + timeWindow: 60 * 1000, + }, + + "/v1/chat/completions": { + max: 5, + timeWindow: 60 * 1000, + }, + + "/v1/models": { + max: 50, + timeWindow: 60 * 1000, + }, + + "/payments/create": { + max: 10, + timeWindow: 60 * 1000, + }, + + "/payments": { + max: 30, + timeWindow: 60 * 1000, + }, +}; + +export function getRateLimitConfigForEnv(): RateLimitOptions { + return { ...RATE_LIMIT_CONFIG }; +} + +export function getRateLimitForRoute(route: string) { + const config = getRateLimitConfigForEnv(); + return ( + config[route] || { + max: 30, + timeWindow: 60 * 1000, + } + ); +} diff --git a/GPTutor-Backend-v2/src/controllers/AuthController.ts b/GPTutor-Backend-v2/src/controllers/AuthController.ts new file mode 100644 index 00000000..7c4156c6 --- /dev/null +++ b/GPTutor-Backend-v2/src/controllers/AuthController.ts @@ -0,0 +1,98 @@ +import { FastifyRequest, FastifyReply } from 'fastify'; +import { BaseController } from './BaseController'; +import { AuthService } from '../services/AuthService'; +import { createVkAuthMiddleware } from '../middleware/authMiddleware'; +import { RequestWithLogging } from '../middleware/loggingMiddleware'; +import { createRateLimitMiddleware, getRateLimitConfig } from '../middleware/rateLimitMiddleware'; + +interface AuthenticatedRequest extends RequestWithLogging { + vkUser: any; + dbUser: any; +} + +export class AuthController extends BaseController { + constructor( + fastify: any, + private authService: AuthService + ) { + super(fastify); + } + + registerRoutes(): void { + const vkAuthMiddleware = createVkAuthMiddleware(this.authService); + + const userRateLimit = createRateLimitMiddleware(getRateLimitConfig('/user')!); + + const updateTokenRateLimit = createRateLimitMiddleware(getRateLimitConfig('/update-token')!); + + this.fastify.get( + '/user', + { + preHandler: [userRateLimit, vkAuthMiddleware] as any + }, + this.getUser.bind(this) + ); + + this.fastify.post( + '/update-token', + { + preHandler: [updateTokenRateLimit, vkAuthMiddleware] as any + }, + this.updateToken.bind(this) + ); + } + + private async getUser(request: any, reply: FastifyReply) { + this.logInfo('User data requested', { + vkId: request.dbUser.vkId, + balance: request.dbUser.balance + }, request); + + return this.sendSuccess(reply, { + message: "User data retrieved successfully!", + vkData: request.vkUser, + user: { + id: request.dbUser.id, + vkId: request.dbUser.vkId, + balance: request.dbUser.balance, + apiKey: request.dbUser.apiKey, + isActive: request.dbUser.isActive, + createdAt: request.dbUser.createdAt, + updatedAt: request.dbUser.updatedAt, + }, + timestamp: new Date().toISOString(), + }); + } + + private async updateToken(request: any, reply: FastifyReply) { + try { + this.logInfo('Token update requested', { + userId: request.dbUser.id, + vkId: request.dbUser.vkId + }, request); + + const updatedUser = await this.authService.updateUserToken(request.dbUser.id); + + this.logInfo('Token updated successfully', { + userId: updatedUser.id, + newApiKey: updatedUser.apiKey.substring(0, 10) + '...' + }, request); + + return this.sendSuccess(reply, { + message: "API token updated successfully!", + newApiKey: updatedUser.apiKey, + user: { + id: updatedUser.id, + vkId: updatedUser.vkId, + balance: updatedUser.balance, + isActive: updatedUser.isActive, + updatedAt: updatedUser.updatedAt, + }, + timestamp: new Date().toISOString(), + }); + } catch (error) { + this.logError('Token update failed', error, request); + return this.sendError(reply, 'Failed to update token', 500); + } + } +} diff --git a/GPTutor-Backend-v2/src/controllers/BaseController.ts b/GPTutor-Backend-v2/src/controllers/BaseController.ts new file mode 100644 index 00000000..21fb7332 --- /dev/null +++ b/GPTutor-Backend-v2/src/controllers/BaseController.ts @@ -0,0 +1,74 @@ +import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { logger } from '../services/LoggerService'; +import { RequestWithLogging } from '../middleware/loggingMiddleware'; + +export abstract class BaseController { + constructor(protected fastify: FastifyInstance) {} + + abstract registerRoutes(): void; + + protected sendSuccess(reply: FastifyReply, data: any, statusCode = 200) { + return reply.code(statusCode).send(data); + } + + protected sendError(reply: FastifyReply, message: string, statusCode = 500, request?: RequestWithLogging) { + if (request) { + logger.warn(`Response error: ${message}`, { + requestId: request.requestId, + userId: request.userId, + statusCode, + url: request.url, + method: request.method + }); + } + return reply.code(statusCode).send({ error: message }); + } + + protected sendValidationError(reply: FastifyReply, message: string, request?: RequestWithLogging) { + return this.sendError(reply, message, 400, request); + } + + protected sendUnauthorized(reply: FastifyReply, message = 'Unauthorized', request?: RequestWithLogging) { + return this.sendError(reply, message, 401, request); + } + + protected sendNotFound(reply: FastifyReply, message = 'Not found', request?: RequestWithLogging) { + return this.sendError(reply, message, 404, request); + } + + protected sendInsufficientBalance(reply: FastifyReply, request?: RequestWithLogging) { + return this.sendError(reply, 'Insufficient balance', 402, request); + } + + protected logInfo(message: string, meta?: any, request?: RequestWithLogging) { + logger.info(message, { + ...meta, + requestId: request?.requestId, + userId: request?.userId + }); + } + + protected logError(message: string, error?: Error | any, meta?: any, request?: RequestWithLogging) { + logger.error(message, error, { + ...meta, + requestId: request?.requestId, + userId: request?.userId + }); + } + + protected logWarn(message: string, meta?: any, request?: RequestWithLogging) { + logger.warn(message, { + ...meta, + requestId: request?.requestId, + userId: request?.userId + }); + } + + protected logDebug(message: string, meta?: any, request?: RequestWithLogging) { + logger.debug(message, { + ...meta, + requestId: request?.requestId, + userId: request?.userId + }); + } +} diff --git a/GPTutor-Backend-v2/src/controllers/CompletionController.ts b/GPTutor-Backend-v2/src/controllers/CompletionController.ts new file mode 100644 index 00000000..9040e372 --- /dev/null +++ b/GPTutor-Backend-v2/src/controllers/CompletionController.ts @@ -0,0 +1,440 @@ +import { FastifyReply } from "fastify"; +import { BaseController } from "./BaseController"; +import { UserRepository } from "../repositories/UserRepository"; +import { LLMCostEvaluate } from "../services/LLMCostEvaluate"; +import { OpenRouterService } from "../services/OpenRouterService"; +import { RequestWithLogging } from "../middleware/loggingMiddleware"; +import { logger } from "../services/LoggerService"; +import { authenticateUser } from "../utils/vkAuth"; +import { + createRateLimitMiddleware, + getRateLimitConfig, +} from "../middleware/rateLimitMiddleware"; + +interface CompletionRequest extends RequestWithLogging { + body: { + model?: string; + messages: Array<{ + role: "system" | "user" | "assistant"; + content: + | string + | Array<{ + type: "text" | "image_url" | "file"; + text?: string; + image_url?: { + url: string; + detail?: "low" | "high" | "auto"; + }; + file?: { + filename: string; + file_data: string; + mimeType?: string; + }; + }>; + }>; + max_tokens?: number; + temperature?: number; + top_p?: number; + frequency_penalty?: number; + presence_penalty?: number; + stop?: string[]; + stream?: boolean; + }; +} + +export class CompletionController extends BaseController { + constructor( + fastify: any, + private userRepository: UserRepository, + private llmCostService: LLMCostEvaluate, + private openRouterService: OpenRouterService, + private vkSecretKey: string = process.env.VK_SECRET_KEY || "" + ) { + super(fastify); + } + + registerRoutes(): void { + const completionsRateLimit = createRateLimitMiddleware( + getRateLimitConfig("/v1/chat/completions")! + ); + + this.fastify.post( + "/v1/chat/completions", + { preHandler: completionsRateLimit }, + this.chatCompletions.bind(this) + ); + } + + private async chatCompletions(request: any, reply: FastifyReply) { + try { + const authHeader = request.headers.authorization; + + const authResult = await authenticateUser( + authHeader, + this.vkSecretKey, + this.userRepository + ); + + if (!authResult) { + return this.sendUnauthorized( + reply, + "Invalid authentication. Use Bearer token (sk-...) or VK authorization.", + request + ); + } + + let user; + let userId; + + if (authResult.authType === "api_key") { + user = authResult.user; + userId = user.id.toString(); + } else if (authResult.authType === "vk") { + const vkData = authResult.user; + let dbUser = await this.userRepository.findByVkId(vkData.vk_user_id); + + if (!dbUser) { + dbUser = await this.userRepository.create({ + vkId: vkData.vk_user_id, + isActive: true, + }); + } + + user = dbUser; + userId = user.id.toString(); + } else { + return this.sendUnauthorized( + reply, + "Unknown authentication type", + request + ); + } + + if (!user.isActive) { + return this.sendUnauthorized( + reply, + "User account is inactive", + request + ); + } + + if (user.balance <= 0) { + this.logInfo( + "Insufficient balance", + { + userId: user.id, + balance: user.balance, + }, + request + ); + + return this.sendInsufficientBalance(reply, request); + } + + request.userId = userId; + + const requestBody = request.body; + if (!requestBody.messages || !Array.isArray(requestBody.messages)) { + return this.sendValidationError( + reply, + "messages array is required", + request + ); + } + + const model = requestBody.model || "google/gemini-2.5-flash-lite"; + + const hasFiles = this.hasFilesInMessages(requestBody.messages); + + this.logInfo( + `LLM request initiated`, + { + model, + messagesCount: requestBody.messages.length, + stream: requestBody.stream || false, + hasFiles, + }, + request + ); + + //todo добавить отнимание баланса + const openRouterParams = { + model, + messages: requestBody.messages, + max_tokens: requestBody.max_tokens, + temperature: requestBody.temperature, + top_p: requestBody.top_p, + frequency_penalty: requestBody.frequency_penalty, + presence_penalty: requestBody.presence_penalty, + stop: requestBody.stop, + ...(hasFiles && { + plugins: [ + { + id: "file-parser", + pdf: { + engine: "native", + }, + }, + ], + }), + }; + + if (requestBody.stream) { + return this.handleStreamingResponse( + reply, + user, + model, + openRouterParams, + request + ); + } + + return this.handleNonStreamingResponse( + reply, + user, + openRouterParams, + request + ); + } catch (error) { + this.logError("Completion API error", error, {}, request); + + if (error instanceof Error) { + if (error.message === "User not found") { + return this.sendUnauthorized(reply, "Invalid API key", request); + } + if (error.message === "Insufficient balance") { + return this.sendInsufficientBalance(reply, request); + } + } + + return this.sendError(reply, "Internal server error", 500, request); + } + } + + private async handleStreamingResponse( + reply: FastifyReply, + user: any, + model: string, + openRouterParams: any, + request: CompletionRequest + ) { + reply.raw.setHeader("Cache-Control", "no-cache"); + reply.raw.setHeader("Connection", "keep-alive"); + reply.raw.setHeader("X-Accel-Buffering", "no"); + reply.raw.setHeader("Access-Control-Allow-Origin", "*"); + reply.raw.setHeader("Content-type", "text/event-stream"); + + let totalCost = 0; + + try { + logger.llmRequest(model, user.id.toString(), undefined, undefined, { + requestId: request.requestId, + stream: true, + }); + + const streamStartTime = Date.now(); + + const stream = await this.openRouterService.createCompletionStream({ + ...openRouterParams, + stream: true, + }); + + let chunkCount = 0; + + for await (const chunk of stream) { + chunkCount++; + + if (chunk.usage) { + const responseUsage = chunk.usage as any; + console.log({ usage: chunk.usage as any }); + const cost = this.llmCostService.calculateCost(responseUsage?.cost); + totalCost = cost; + + chunk.usage = { + prompt_tokens: responseUsage?.prompt_tokens, + completion_tokens: responseUsage?.completion_tokens, + total_tokens: responseUsage?.total_tokens, + cost, + } as any; + } + + const chunkData = `data: ${JSON.stringify(chunk)}\n\n`; + reply.raw.write(chunkData); + + if (chunkCount % 50 === 0) { + this.logDebug( + `Streaming progress: ${chunkCount} chunks sent`, + {}, + request + ); + } + } + + reply.raw.write("data: [DONE]\n\n"); + + reply.raw.end(); + + const streamDuration = Date.now() - streamStartTime; + + logger.llmResponse( + model, + user.id.toString(), + 0, + totalCost, + streamDuration, + { + requestId: request.requestId, + chunks: chunkCount, + stream: true, + } + ); + + await this.userRepository.decreaseBalance(user.id, totalCost); + + return; + } catch (streamError) { + this.logError("Stream error", streamError, { model }, request); + + try { + reply.raw.write( + `data: ${JSON.stringify({ + error: "Stream error", + message: + streamError instanceof Error + ? streamError.message + : "Unknown error", + })}\n\n` + ); + reply.raw.write("data: [DONE]\n\n"); + reply.raw.end(); + } catch (writeError) { + this.logError( + "Error writing error response to stream", + writeError, + {}, + request + ); + } + + return; + } + } + + private async handleNonStreamingResponse( + reply: FastifyReply, + user: any, + openRouterParams: any, + request: CompletionRequest + ) { + const requestStartTime = Date.now(); + + logger.llmRequest( + openRouterParams.model, + user.id.toString(), + undefined, + undefined, + { + requestId: request.requestId, + stream: false, + } + ); + + const completion = await this.openRouterService.createCompletion({ + ...openRouterParams, + stream: false, + }); + + console.log(completion); + const responseUsage = completion.usage; + console.log({ usage: responseUsage }); + const originalCostUsd = (completion.usage as any)?.cost; + const cost = this.llmCostService.calculateCost(originalCostUsd); + const requestDuration = Date.now() - requestStartTime; + const totalTokens = (completion.usage as any)?.total_tokens || 0; + + await this.userRepository.decreaseBalance(user.id, cost); + + const updatedUser = + (await this.userRepository.findByVkId(user.vkId || "")) || + (await this.userRepository.findByApiKey( + request.headers.authorization?.substring(7) || "" + )); + + logger.llmResponse( + openRouterParams.model, + user.id.toString(), + totalTokens, + cost, + requestDuration, + { + requestId: request.requestId, + originalCostUsd, + remainingBalance: updatedUser?.balance, + } + ); + + logger.balance( + "decrease", + user.id.toString(), + cost, + updatedUser?.balance || 0, + { + requestId: request.requestId, + reason: "llm_completion", + } + ); + + const responseWithCost = { + ...completion, + usage: { + prompt_tokens: responseUsage?.prompt_tokens, + completion_tokens: responseUsage?.completion_tokens, + total_tokens: responseUsage?.total_tokens, + cost, + }, + }; + + return this.sendSuccess(reply, responseWithCost); + } + + /** + * Проверяет наличие файлов в сообщениях + */ + private hasFilesInMessages( + messages: Array<{ + role: "system" | "user" | "assistant"; + content: + | string + | Array<{ + type: "text" | "image_url" | "file"; + text?: string; + image_url?: { + url: string; + detail?: "low" | "high" | "auto"; + }; + file?: { + filename: string; + file_data: string; + mimeType?: string; + }; + }>; + }> + ): boolean { + return messages.some((message) => { + // Если content это строка, файлов нет + if (typeof message.content === "string") { + return false; + } + + // Если content это массив, проверяем каждый элемент + if (Array.isArray(message.content)) { + return message.content.some( + (contentItem) => + contentItem.type === "image_url" || contentItem.type === "file" + ); + } + + return false; + }); + } +} diff --git a/GPTutor-Backend-v2/src/controllers/FilesController.ts b/GPTutor-Backend-v2/src/controllers/FilesController.ts new file mode 100644 index 00000000..b30e40de --- /dev/null +++ b/GPTutor-Backend-v2/src/controllers/FilesController.ts @@ -0,0 +1,230 @@ +import { FastifyRequest, FastifyReply } from "fastify"; +import { BaseController } from "./BaseController"; +import { FilesService } from "../services/FilesService"; +import { FileRepository } from "../repositories/FileRepository"; +import { RequestWithLogging } from "../middleware/loggingMiddleware"; +import { createVkAuthMiddleware } from "../middleware/authMiddleware"; +import { AuthService } from "../services/AuthService"; +import { + createRateLimitMiddleware, + getRateLimitConfig, +} from "../middleware/rateLimitMiddleware"; + +interface FileUploadRequest extends RequestWithLogging { + vkUser: any; + dbUser: any; +} + +export class FilesController extends BaseController { + constructor( + fastify: any, + private filesService: FilesService, + private fileRepository: FileRepository, + private authService: AuthService + ) { + super(fastify); + } + + registerRoutes(): void { + const vkAuthMiddleware = createVkAuthMiddleware(this.authService); + + const uploadRateLimit = createRateLimitMiddleware( + getRateLimitConfig("/upload")! + ); + + this.fastify.post( + "/upload", + { + preHandler: [uploadRateLimit, vkAuthMiddleware] as any, + config: { + bodyLimit: 50 * 1024 * 1024, + }, + }, + this.uploadFile.bind(this) + ); + } + + private async uploadFile(request: any, reply: FastifyReply) { + try { + this.logInfo("File upload requested", { + userId: request.dbUser.id, + vkId: request.dbUser.vkId, + }); + + const data = await (request as any).file(); + + if (!data) { + return this.sendError(reply, "No file provided", 400, request); + } + + const maxSize = 50 * 1024 * 1024; + if (data.file.bytesRead > maxSize) { + return this.sendError( + reply, + "File too large. Maximum size is 50MB", + 413, + request + ); + } + + const fileBuffer = await data.toBuffer(); + const arrayBuffer = fileBuffer.buffer.slice( + fileBuffer.byteOffset, + fileBuffer.byteOffset + fileBuffer.byteLength + ); + + this.logInfo("Processing file", { + fileName: data.filename, + fileSize: data.file.bytesRead, + mimeType: data.mimetype, + }); + + const existingFile = + await this.fileRepository.findByNameAndSizeOrOriginal( + data.filename, + data.file.bytesRead + ); + + if (existingFile) { + this.logInfo("File already exists, returning cached file", { + fileId: existingFile.id, + fileName: existingFile.name, + url: existingFile.url, + wasConverted: existingFile.converted, + originalName: existingFile.originalName, + }); + + return this.sendSuccess(reply, { + message: existingFile.converted + ? "File already converted and cached!" + : "File already exists!", + file: { + id: existingFile.id, + name: existingFile.name, + type: existingFile.type, + url: existingFile.url, + size: existingFile.size, + createdAt: existingFile.createdAt, + }, + converted: existingFile.converted, + fromCache: true, + timestamp: new Date().toISOString(), + }); + } + + const result = await this.filesService.optimizeAndUploadFile( + arrayBuffer, + data.filename + ); + + let finalMimeType = data.mimetype; + let finalFileName = data.filename; + let wasConverted = false; + + if (result.finalFileName !== data.filename) { + wasConverted = true; + finalMimeType = "application/pdf"; + finalFileName = result.finalFileName; + + this.logInfo("File was converted to PDF", { + originalName: data.filename, + originalType: data.mimetype, + originalSize: data.file.bytesRead, + finalName: finalFileName, + finalType: finalMimeType, + }); + } + + // Сохраняем файл с информацией об оригинале (если был конвертирован) + const savedFile = await this.fileRepository.create({ + userId: request.dbUser.id, + type: finalMimeType, + name: finalFileName, + url: result.url, + size: data.file.bytesRead, // Сохраняем оригинальный размер для правильного кеширования + originalName: wasConverted ? data.filename : undefined, + originalSize: wasConverted ? data.file.bytesRead : undefined, + converted: wasConverted, + }); + + this.logInfo("File uploaded successfully", { + fileId: savedFile.id, + fileName: savedFile.name, + url: savedFile.url, + userId: request.dbUser.id, + wasConverted: wasConverted, + originalName: savedFile.originalName, + originalSize: savedFile.originalSize, + }); + + return this.sendSuccess(reply, { + message: wasConverted + ? "File converted to PDF and uploaded successfully!" + : "File uploaded successfully!", + file: { + id: savedFile.id, + name: savedFile.name, + type: savedFile.type, + url: savedFile.url, + size: savedFile.size, + createdAt: savedFile.createdAt, + }, + converted: wasConverted, + fromCache: false, + ...(wasConverted && { + originalFile: { + name: savedFile.originalName, + size: savedFile.originalSize, + }, + }), + timestamp: new Date().toISOString(), + }); + } catch (error) { + this.logError("File upload failed", error); + + if (error instanceof Error) { + const errorMessage = error.message; + + if (errorMessage.includes("Unknown or unsupported file type")) { + return this.sendError(reply, "Unsupported file type", 400, request); + } + + if (errorMessage.includes("Invalid filename")) { + return this.sendError(reply, "Invalid filename", 400, request); + } + + if (errorMessage.includes("LibreOffice not found")) { + return this.sendError( + reply, + "Document conversion requires LibreOffice. Please contact administrator.", + 503, + request + ); + } + + if ( + errorMessage.includes("Failed to read document") || + errorMessage.includes("corrupted") + ) { + return this.sendError( + reply, + "Failed to convert document. The file may be corrupted or password-protected.", + 400, + request + ); + } + + if (errorMessage.includes("Failed to convert document to PDF")) { + return this.sendError( + reply, + "Failed to convert document to PDF. Please try again or use a different file.", + 400, + request + ); + } + } + + return this.sendError(reply, "Failed to upload file", 500, request); + } + } +} diff --git a/GPTutor-Backend-v2/src/controllers/HealthController.ts b/GPTutor-Backend-v2/src/controllers/HealthController.ts new file mode 100644 index 00000000..7ebec861 --- /dev/null +++ b/GPTutor-Backend-v2/src/controllers/HealthController.ts @@ -0,0 +1,22 @@ +import { FastifyRequest, FastifyReply } from 'fastify'; +import { BaseController } from './BaseController'; +import { createRateLimitMiddleware, getRateLimitConfig } from '../middleware/rateLimitMiddleware'; + +export class HealthController extends BaseController { + registerRoutes(): void { + const healthRateLimit = createRateLimitMiddleware(getRateLimitConfig('/health')!); + + this.fastify.get( + '/health', + { preHandler: healthRateLimit }, + this.healthCheck.bind(this) + ); + } + + private async healthCheck(request: FastifyRequest, reply: FastifyReply) { + return this.sendSuccess(reply, { + status: "ok", + timestamp: new Date().toISOString() + }); + } +} diff --git a/GPTutor-Backend-v2/src/controllers/ModelsController.ts b/GPTutor-Backend-v2/src/controllers/ModelsController.ts new file mode 100644 index 00000000..d61d1b72 --- /dev/null +++ b/GPTutor-Backend-v2/src/controllers/ModelsController.ts @@ -0,0 +1,155 @@ +import { FastifyRequest, FastifyReply } from "fastify"; +import { BaseController } from "./BaseController"; +import { LLMCostEvaluate } from "../services/LLMCostEvaluate"; +import { RequestWithLogging } from "../middleware/loggingMiddleware"; +import { + createRateLimitMiddleware, + getRateLimitConfig, +} from "../middleware/rateLimitMiddleware"; + +export class ModelsController extends BaseController { + constructor(protected fastify: any, private llmCostService: LLMCostEvaluate) { + super(fastify); + } + + registerRoutes(): void { + // Rate limiting для Models endpoint + const modelsRateLimit = createRateLimitMiddleware( + getRateLimitConfig("/v1/models")! + ); + + this.fastify.get( + "/v1/models", + { preHandler: modelsRateLimit }, + this.getModels.bind(this) + ); + } + + private async getModels(request: FastifyRequest, reply: FastifyReply) { + try { + this.logInfo( + "Getting popular provider Models", + {}, + request as RequestWithLogging + ); + + // Проверяем, что сервис инициализирован + const allModels = this.llmCostService.getAllModels(); + if (!allModels || allModels.length === 0) { + this.logWarn( + "LLM Cost Service not initialized or no Models loaded", + {}, + request as RequestWithLogging + ); + return this.sendError( + reply, + "Models service not ready", + 503, + request as RequestWithLogging + ); + } + + const models = this.llmCostService.getPopularProviderModels(); + + // Проверяем, что модели загружены корректно + if (!models || models.length === 0) { + this.logWarn( + "No Models found from popular providers", + {}, + request as RequestWithLogging + ); + + return this.sendSuccess(reply, { + success: true, + data: { + models: [], + total: 0, + providers: [ + "x-ai", + "deepseek", + "google", + "qwen", + "perplexity", + "mistralai", + "openai", + "anthropic", + ], + lastUpdated: new Date().toISOString(), + }, + }); + } + + this.logInfo( + `Found ${models.length} models from popular providers`, + { + modelCount: models.length, + sampleModel: models[0] + ? { + id: models[0].id, + hasPricingRub: !!models[0].pricing_rub, + hasPrompt: !!models[0].pricing_rub?.prompt, + } + : null, + }, + request as RequestWithLogging + ); + + const sortedModels = models.sort((a, b) => { + // Безопасное получение цены с проверкой на существование + const priceA = a.pricing_rub?.prompt || 0; + const priceB = b.pricing_rub?.prompt || 0; + + // Сначала сравниваем по цене (самые дорогие сверху) + const priceDiff = priceB - priceA; + if (priceDiff !== 0) { + return priceDiff; + } + + // Если цены равны, сравниваем по дате создания (самые новые сверху) + const createdA = a.created || 0; + const createdB = b.created || 0; + return createdB - createdA; + }); + + this.logInfo( + `Retrieved ${sortedModels.length} popular models (sorted by price and creation date)`, + { + modelCount: sortedModels.length, + }, + request as RequestWithLogging + ); + + return this.sendSuccess(reply, { + success: true, + data: { + models: sortedModels, + total: sortedModels.length, + providers: [ + "x-ai", + "deepseek", + "google", + "qwen", + "perplexity", + "mistralai", + "openai", + "anthropic", + ], + lastUpdated: new Date().toISOString(), + }, + }); + } catch (error) { + this.logError( + "Failed to get Models", + error, + {}, + request as RequestWithLogging + ); + return this.sendError( + reply, + "Failed to retrieve Models", + 500, + request as RequestWithLogging + ); + } + } +} diff --git a/GPTutor-Backend-v2/src/controllers/PaymentController.ts b/GPTutor-Backend-v2/src/controllers/PaymentController.ts new file mode 100644 index 00000000..26eec20f --- /dev/null +++ b/GPTutor-Backend-v2/src/controllers/PaymentController.ts @@ -0,0 +1,266 @@ +import { FastifyRequest, FastifyReply } from "fastify"; +import { BaseController } from "./BaseController"; +import { YooKassaService } from "../services/YooKassaService"; +import { createVkAuthMiddleware } from "../middleware/authMiddleware"; +import { AuthService } from "../services/AuthService"; +import { RequestWithLogging } from "../middleware/loggingMiddleware"; +import { + createRateLimitMiddleware, + getRateLimitConfig, +} from "../middleware/rateLimitMiddleware"; +import { yookassaIpCheck } from "../middleware/yookassaIpMiddleware"; + +interface AuthenticatedRequest extends RequestWithLogging { + vkUser: any; + dbUser: any; +} + +interface CreatePaymentBody { + amount: number; + description?: string; + returnUrl?: string; +} + +interface PaymentWebhookBody { + type: string; + event: string; + object: any; +} + +export class PaymentController extends BaseController { + constructor( + fastify: any, + private yooKassaService: YooKassaService, + private authService: AuthService + ) { + super(fastify); + } + + registerRoutes(): void { + const vkAuthMiddleware = createVkAuthMiddleware(this.authService); + const createPaymentRateLimit = createRateLimitMiddleware( + getRateLimitConfig("/payments/create")! + ); + const getPaymentsRateLimit = createRateLimitMiddleware( + getRateLimitConfig("/payments")! + ); + + // Создание платежа + this.fastify.post( + "/payments/create", + { + preHandler: [createPaymentRateLimit, vkAuthMiddleware] as any, + }, + this.createPayment.bind(this) + ); + + // Получение списка платежей пользователя + this.fastify.get( + "/payments", + { + preHandler: [getPaymentsRateLimit, vkAuthMiddleware] as any, + }, + this.getUserPayments.bind(this) + ); + + this.fastify.post( + "/payments/webhook", + { + preHandler: [yookassaIpCheck] as any, + }, + this.handleWebhook.bind(this) + ); + + this.fastify.get( + "/payments/:yookassaId", + { + preHandler: [vkAuthMiddleware] as any, + }, + this.getPaymentInfo.bind(this) + ); + } + + private async createPayment(request: any, reply: FastifyReply) { + try { + const { amount, description, returnUrl } = + request.body as CreatePaymentBody; + + if (!amount || typeof amount !== "number") { + return this.sendValidationError( + reply, + "Amount is required and must be a number", + request + ); + } + + if (amount < 70) { + return this.sendValidationError( + reply, + "Minimum payment amount is 70₽", + request + ); + } + + if (amount > 100000) { + return this.sendValidationError( + reply, + "Maximum payment amount is 100,000₽", + request + ); + } + + this.logInfo( + "Creating payment", + { + userId: request.dbUser.id, + amount, + vkId: request.dbUser.vkId, + }, + request + ); + + const payment = await this.yooKassaService.createPayment({ + userId: request.dbUser.id, + amount, + description, + returnUrl, + }); + + this.logInfo( + "Payment created successfully", + { + paymentId: payment.id, + yookassaId: payment.yookassaId, + amount: payment.amount, + }, + request + ); + + return this.sendSuccess( + reply, + { + message: "Payment created successfully", + payment: { + id: payment.id, + yookassaId: payment.yookassaId, + amount: payment.amount, + status: payment.status, + confirmationUrl: payment.confirmationUrl, + createdAt: payment.createdAt, + }, + }, + 201 + ); + } catch (error) { + this.logError( + "Failed to create payment", + error, + { + userId: request.dbUser?.id, + }, + request + ); + return this.sendError( + reply, + error instanceof Error ? error.message : "Failed to create payment", + 500, + request + ); + } + } + + private async getUserPayments(request: any, reply: FastifyReply) { + try { + this.logInfo( + "Fetching user payments", + { + userId: request.dbUser.id, + }, + request + ); + + const payments = await this.yooKassaService.getUserPayments( + request.dbUser.id + ); + + return this.sendSuccess(reply, { + message: "Payments retrieved successfully", + payments, + }); + } catch (error) { + this.logError( + "Failed to get user payments", + error, + { + userId: request.dbUser?.id, + }, + request + ); + return this.sendError(reply, "Failed to retrieve payments", 500, request); + } + } + + private async getPaymentInfo(request: any, reply: FastifyReply) { + try { + const { yookassaId } = request.params as { yookassaId: string }; + + this.logInfo( + "Fetching payment info", + { + yookassaId, + userId: request.dbUser.id, + }, + request + ); + + const payment = await this.yooKassaService.getPaymentInfo(yookassaId); + + return this.sendSuccess(reply, { + message: "Payment info retrieved successfully", + payment, + }); + } catch (error) { + this.logError("Failed to get payment info", error, request); + return this.sendError( + reply, + "Failed to retrieve payment info", + 500, + request + ); + } + } + + private async handleWebhook(request: FastifyRequest, reply: FastifyReply) { + try { + const webhookData = request.body as PaymentWebhookBody; + + this.logInfo("Processing payment webhook", { + type: webhookData.type, + event: webhookData.event, + paymentId: webhookData.object?.id, + }); + + if ( + webhookData.event === "payment.succeeded" || + webhookData.event === "payment.canceled" + ) { + const result = await this.yooKassaService.handlePaymentWebhook( + webhookData + ); + + this.logInfo("Webhook processed successfully", { + success: result.success, + paymentId: result.paymentId, + status: result.status, + }); + + return this.sendSuccess(reply, result); + } + + return this.sendSuccess(reply, { message: "Webhook received" }); + } catch (error) { + this.logError("Failed to process webhook", error); + return this.sendError(reply, "Failed to process webhook", 500); + } + } +} diff --git a/GPTutor-Backend-v2/src/controllers/index.ts b/GPTutor-Backend-v2/src/controllers/index.ts new file mode 100644 index 00000000..114f084d --- /dev/null +++ b/GPTutor-Backend-v2/src/controllers/index.ts @@ -0,0 +1,63 @@ +import { FastifyInstance } from 'fastify'; +import { HealthController } from './HealthController'; +import { AuthController } from './AuthController'; +import { CompletionController } from './CompletionController'; +import { ModelsController } from './ModelsController'; +import { FilesController } from './FilesController'; +import { PaymentController } from './PaymentController'; +import { AuthService } from '../services/AuthService'; +import { UserRepository } from '../repositories/UserRepository'; +import { FileRepository } from '../repositories/FileRepository'; +import { LLMCostEvaluate } from '../services/LLMCostEvaluate'; +import { OpenRouterService } from '../services/OpenRouterService'; +import { FilesService } from '../services/FilesService'; +import { YooKassaService } from '../services/YooKassaService'; + +export function registerControllers( + fastify: FastifyInstance, + dependencies: { + authService: AuthService; + userRepository: UserRepository; + fileRepository: FileRepository; + filesService: FilesService; + llmCostService: LLMCostEvaluate; + openRouterService: OpenRouterService; + yooKassaService: YooKassaService; + } +) { + const controllers = [ + new HealthController(fastify), + new AuthController(fastify, dependencies.authService), + new CompletionController( + fastify, + dependencies.userRepository, + dependencies.llmCostService, + dependencies.openRouterService + ), + new ModelsController( + fastify, + dependencies.llmCostService + ), + new FilesController( + fastify, + dependencies.filesService, + dependencies.fileRepository, + dependencies.authService + ), + new PaymentController( + fastify, + dependencies.yooKassaService, + dependencies.authService + ), + ]; + + controllers.forEach(controller => controller.registerRoutes()); +} + +export * from './BaseController'; +export * from './HealthController'; +export * from './AuthController'; +export * from './CompletionController'; +export * from './ModelsController'; +export * from './FilesController'; +export * from './PaymentController'; diff --git a/GPTutor-Backend-v2/src/index.ts b/GPTutor-Backend-v2/src/index.ts new file mode 100644 index 00000000..77cbb4ce --- /dev/null +++ b/GPTutor-Backend-v2/src/index.ts @@ -0,0 +1,195 @@ +import Fastify from "fastify"; +import { PrismaClient } from "@prisma/client"; +import { UserRepository } from "./repositories/UserRepository"; +import { FileRepository } from "./repositories/FileRepository"; +import { PaymentRepository } from "./repositories/PaymentRepository"; +import { AuthService } from "./services/AuthService"; +import { FilesService } from "./services/FilesService"; +import { FileCleanupService } from "./services/FileCleanupService"; +import { LLMCostEvaluate } from "./services/LLMCostEvaluate"; +import { OpenRouterService } from "./services/OpenRouterService"; +import { YooKassaService } from "./services/YooKassaService"; +import { logger } from "./services/LoggerService"; +import { registerControllers } from "./controllers"; + +const prisma = new PrismaClient(); +const userRepository = new UserRepository(prisma); +const fileRepository = new FileRepository(prisma); +const paymentRepository = new PaymentRepository(prisma); + +console.log(process.env); + +const authService = new AuthService( + userRepository, + process.env.VK_APP_ID!, + process.env.VK_SECRET_KEY! +); +const filesService = new FilesService(); +const fileCleanupService = new FileCleanupService(prisma, filesService); +const llmCostService = new LLMCostEvaluate(100); +const openRouterService = new OpenRouterService( + process.env.OPENROUTER_API_KEY! +); +const yooKassaService = new YooKassaService( + process.env.YOOKASSA_SHOP_ID || "1184807", + process.env.YOOKASSA_SECRET_KEY || "test_XnTIPN_kQjZoX3ZYAbJ4DL6Z3Q-vo_4uXqWtf81-dm4", + paymentRepository, + userRepository +); + +const fastify = Fastify({ + logger: true, + disableRequestLogging: true, + bodyLimit: 80 * 1024 * 1024, // 80 МБ лимит для тела запроса + trustProxy: true, // Доверяем заголовкам от reverse proxy (Traefik) +}); + +fastify.register(require("@fastify/cors"), { + origin: "*", + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allowedHeaders: [ + "Content-Type", + "Authorization", + "Accept", + "Cache-Control", + "Pragma", + ], + credentials: true, + optionsSuccessStatus: 200, +}); + +fastify.register(require("@fastify/helmet")); + +fastify.register(require("@fastify/rate-limit"), { + max: 100, + timeWindow: 60 * 1000, + keyGenerator: (request: any) => { + const ip = request.ip; + const userId = request.userId || "anonymous"; + return `rate_limit:${ip}:${userId}`; + }, + errorResponseBuilder: (request: any, context: any) => { + return { + error: "Too Many Requests", + message: `Rate limit exceeded. Maximum ${context.max} requests per ${ + context.timeWindow / 1000 + } seconds.`, + retryAfter: Math.ceil(context.after / 1000), + }; + }, +}); + +fastify.register(require("@fastify/multipart"), { + limits: { + fileSize: 50 * 1024 * 1024, + }, +}); + +fastify.addHook("preHandler", async (request: any, reply) => { + request.requestId = require("uuid").v4(); + request.startTime = Date.now(); + logger.apiRequest(request.method, request.url, request.userId, { + requestId: request.requestId, + userAgent: request.headers["user-agent"], + ip: request.ip, + }); +}); + +fastify.addHook("onResponse", async (request: any, reply) => { + const duration = Date.now() - (request.startTime || 0); + logger.apiResponse( + request.method, + request.url, + reply.statusCode, + duration, + request.userId, + { + requestId: request.requestId, + } + ); +}); + +fastify.setErrorHandler(async (error, request: any, reply) => { + const duration = Date.now() - (request.startTime || 0); + logger.error(`Request failed: ${request.method} ${request.url}`, error, { + requestId: request.requestId, + userId: request.userId, + duration, + statusCode: reply.statusCode, + }); + throw error; +}); + +registerControllers(fastify, { + authService, + userRepository, + fileRepository, + filesService, + llmCostService, + openRouterService, + yooKassaService, +}); + +const start = async () => { + try { + logger.info("Starting GPTutor Backend v2..."); + + await llmCostService.initialize(); + + fileCleanupService.start(); + + await fastify.listen({ + port: Number(process.env.PORT) || 3001, + host: "0.0.0.0", + }); + + logger.info("🚀 Server is running", { + port: Number(process.env.PORT) || 3001, + host: "0.0.0.0", + environment: process.env.NODE_ENV || "development", + }); + } catch (err) { + logger.error("Failed to start server", err); + process.exit(1); + } +}; + +process.on("SIGINT", async () => { + logger.info("Received SIGINT, shutting down gracefully..."); + try { + fileCleanupService.stop(); + await fastify.close(); + await prisma.$disconnect(); + logger.info("Server shut down successfully"); + process.exit(0); + } catch (error) { + logger.error("Error during shutdown", error); + process.exit(1); + } +}); + +process.on("SIGTERM", async () => { + logger.info("Received SIGTERM, shutting down gracefully..."); + try { + fileCleanupService.stop(); + await fastify.close(); + await prisma.$disconnect(); + logger.info("Server shut down successfully"); + process.exit(0); + } catch (error) { + logger.error("Error during shutdown", error); + process.exit(1); + } +}); + +process.on("uncaughtException", (error) => { + logger.error("Uncaught Exception", error); + process.exit(1); +}); + +process.on("unhandledRejection", (reason, promise) => { + logger.error("Unhandled Rejection", reason, { promise }); + process.exit(1); +}); + +start(); diff --git a/GPTutor-Backend-v2/src/middleware/authMiddleware.ts b/GPTutor-Backend-v2/src/middleware/authMiddleware.ts new file mode 100644 index 00000000..7c37baf4 --- /dev/null +++ b/GPTutor-Backend-v2/src/middleware/authMiddleware.ts @@ -0,0 +1,45 @@ +import { FastifyRequest, FastifyReply } from "fastify"; +import { AuthService } from "../services/AuthService"; +import { RequestWithLogging } from "./loggingMiddleware"; +import { logger } from "../services/LoggerService"; + +export function createVkAuthMiddleware(authService: AuthService) { + return async function vkAuthMiddleware( + request: RequestWithLogging, + reply: FastifyReply + ) { + const authHeader = request.headers.authorization; + + if (!authHeader || !authHeader.startsWith("Bearer ")) { + reply + .code(401) + .send({ error: "Missing or invalid authorization header" }); + return; + } + + const token = authHeader.substring(7); // Remove "Bearer " + + try { + const authResult = await authService.authorizeVKUser(token); + (request as any).vkUser = authResult.vkData; + (request as any).dbUser = authResult.dbUser; + + request.userId = authResult.dbUser.id.toString(); + + logger.auth("vk_authorize", request.userId, true, { + requestId: request.requestId, + vkId: (authResult.vkData as any).id, + }); + } catch (error) { + logger.auth("vk_authorize", request.userId, false, { + requestId: request.requestId, + error: error instanceof Error ? error.message : "Unknown error", + }); + + reply.code(401).send({ + error: error instanceof Error ? error.message : "Authorization failed", + }); + return; + } + }; +} diff --git a/GPTutor-Backend-v2/src/middleware/loggingMiddleware.ts b/GPTutor-Backend-v2/src/middleware/loggingMiddleware.ts new file mode 100644 index 00000000..74f59430 --- /dev/null +++ b/GPTutor-Backend-v2/src/middleware/loggingMiddleware.ts @@ -0,0 +1,64 @@ +import { FastifyRequest, FastifyReply } from 'fastify'; +import { logger } from '../services/LoggerService'; +import { v4 as uuidv4 } from 'uuid'; + +export interface RequestWithLogging extends FastifyRequest { + requestId: string; + startTime: number; + userId?: string; +} + +export function createLoggingMiddleware() { + return async function loggingMiddleware(request: RequestWithLogging, reply: FastifyReply) { + // Generate unique request ID + request.requestId = uuidv4(); + request.startTime = Date.now(); + + // Try to extract user ID from various sources + const authHeader = request.headers.authorization; + if (authHeader && authHeader.startsWith('Bearer ')) { + // This will be set by auth middleware later, but we can try to extract it + request.userId = 'unknown'; // Will be updated by auth middleware + } + + // Log incoming request + logger.apiRequest( + request.method, + request.url, + request.userId, + { + requestId: request.requestId, + userAgent: request.headers['user-agent'], + ip: request.ip, + headers: { + 'content-type': request.headers['content-type'], + 'authorization': authHeader ? 'Bearer [REDACTED]' : undefined + } + } + ); + + // We'll log response in the main request handler instead + // since Fastify's hook system has complex typing + + // Add request ID to response headers for tracking + reply.header('X-Request-ID', request.requestId); + }; +} + +export function createErrorLoggingMiddleware() { + return async function errorLoggingMiddleware(error: Error, request: RequestWithLogging, reply: FastifyReply) { + const duration = Date.now() - request.startTime; + + logger.error(`Request failed: ${request.method} ${request.url}`, error, { + requestId: request.requestId, + userId: request.userId, + duration, + statusCode: reply.statusCode, + userAgent: request.headers['user-agent'], + ip: request.ip + }); + + // Don't modify the error, just log it + throw error; + }; +} diff --git a/GPTutor-Backend-v2/src/middleware/rateLimitMiddleware.test.ts b/GPTutor-Backend-v2/src/middleware/rateLimitMiddleware.test.ts new file mode 100644 index 00000000..40813f80 --- /dev/null +++ b/GPTutor-Backend-v2/src/middleware/rateLimitMiddleware.test.ts @@ -0,0 +1,76 @@ +import { createRateLimitMiddleware, cleanupRateLimitStore, getGlobalRateLimitStore } from './rateLimitMiddleware'; + +// Простой тест для проверки работы rate limiting +async function testRateLimitMiddleware() { + console.log('🧪 Тестирование rate limiting middleware...'); + + const config = { + max: 3, + timeWindow: 1000, // 1 секунда для быстрого тестирования + }; + + const middleware = createRateLimitMiddleware(config); + + // Создаем мок объекты + const mockRequest = { + ip: '127.0.0.1', + url: '/test', + method: 'GET', + headers: { 'user-agent': 'test-agent' } + } as any; + + const mockReply = { + code: (status: number) => ({ + send: (data: any) => { + console.log(`Response ${status}:`, data); + return { status, data }; + } + }), + header: (name: string, value: string) => { + console.log(`Header ${name}: ${value}`); + } + } as any; + + console.log('\n📊 Тестирование нормальных запросов...'); + + // Тест 1-3: нормальные запросы + for (let i = 1; i <= 3; i++) { + console.log(`Запрос ${i}:`); + await middleware(mockRequest, mockReply); + } + + console.log('\n🚫 Тестирование превышения лимита...'); + + // Тест 4: превышение лимита + console.log('Запрос 4 (должен быть заблокирован):'); + await middleware(mockRequest, mockReply); + + console.log('\n🧹 Тестирование очистки store...'); + + // Проверяем размер store до очистки + const store = getGlobalRateLimitStore(); + console.log(`Store size before cleanup: ${store.size}`); + + // Ждем истечения времени окна + console.log('Ожидание истечения timeWindow...'); + await new Promise(resolve => setTimeout(resolve, 1100)); + + // Очищаем store + cleanupRateLimitStore(store); + console.log(`Store size after cleanup: ${store.size}`); + + console.log('\n✅ Тест после очистки...'); + + // Тест после очистки - должен работать снова + console.log('Запрос после очистки:'); + await middleware(mockRequest, mockReply); + + console.log('\n🎉 Тестирование завершено!'); +} + +// Запуск теста, если файл выполняется напрямую +if (require.main === module) { + testRateLimitMiddleware().catch(console.error); +} + +export { testRateLimitMiddleware }; diff --git a/GPTutor-Backend-v2/src/middleware/rateLimitMiddleware.ts b/GPTutor-Backend-v2/src/middleware/rateLimitMiddleware.ts new file mode 100644 index 00000000..e7d4cc50 --- /dev/null +++ b/GPTutor-Backend-v2/src/middleware/rateLimitMiddleware.ts @@ -0,0 +1,127 @@ +import { FastifyRequest, FastifyReply } from "fastify"; +import { logger } from "../services/LoggerService"; +import { getRateLimitForRoute } from "../config/rateLimitConfig"; + +export interface RateLimitConfig { + max: number; + timeWindow: number; + keyGenerator?: (request: FastifyRequest) => string; + onLimitReached?: (request: FastifyRequest, reply: FastifyReply) => void; + skipSuccessfulRequests?: boolean; + skipFailedRequests?: boolean; +} + +export interface RateLimitOptions { + [route: string]: RateLimitConfig; +} + +export function generateRateLimitKey(request: FastifyRequest): string { + const ip = request.ip; + const userId = (request as any).userId || "anonymous"; + const userAgent = request.headers["user-agent"] || "unknown"; + + return `rate_limit:${ip}:${userId}:${Buffer.from(userAgent) + .toString("base64") + .slice(0, 10)}`; +} + +export function generateIPRateLimitKey(request: FastifyRequest): string { + const ip = request.ip; + return `rate_limit:${ip}`; +} + +const globalRateLimitStore = new Map< + string, + { count: number; resetTime: number } +>(); + +export function createRateLimitMiddleware(config: RateLimitConfig) { + const store = globalRateLimitStore; + + return async (request: FastifyRequest, reply: FastifyReply) => { + const key = config.keyGenerator + ? config.keyGenerator(request) + : generateIPRateLimitKey(request); + const now = Date.now(); + + let record = store.get(key); + + if (!record || now > record.resetTime) { + record = { + count: 0, + resetTime: now + config.timeWindow, + }; + store.set(key, record); + } + + record.count++; + + if (record.count > config.max) { + logger.warn("Rate limit exceeded", { + ip: request.ip, + url: request.url, + method: request.method, + key, + count: record.count, + max: config.max, + timeWindow: config.timeWindow, + }); + + if (config.onLimitReached) { + config.onLimitReached(request, reply); + } else { + reply.code(429).send({ + error: "Too Many Requests", + message: `Rate limit exceeded. Maximum ${config.max} requests per ${ + config.timeWindow / 1000 + } seconds.`, + retryAfter: Math.ceil((record.resetTime - now) / 1000), + }); + } + return; + } + + reply.header("X-RateLimit-Limit", config.max.toString()); + reply.header( + "X-RateLimit-Remaining", + Math.max(0, config.max - record.count).toString() + ); + reply.header( + "X-RateLimit-Reset", + Math.ceil(record.resetTime / 1000).toString() + ); + + store.set(key, record); + }; +} + +export function getRateLimitConfig(route: string): RateLimitConfig | null { + const env = process.env.NODE_ENV || "production"; + return getRateLimitForRoute(route); +} + +export function cleanupRateLimitStore( + store: Map +) { + const now = Date.now(); + let cleanedCount = 0; + + for (const [key, record] of store.entries()) { + if (now > record.resetTime) { + store.delete(key); + cleanedCount++; + } + } + + if (cleanedCount > 0) { + logger.debug(`Cleaned up ${cleanedCount} expired rate limit records`); + } +} + +export function getGlobalRateLimitStore() { + return globalRateLimitStore; +} + +setInterval(() => { + cleanupRateLimitStore(globalRateLimitStore); +}, 5 * 60 * 1000); diff --git a/GPTutor-Backend-v2/src/middleware/yookassaIpMiddleware.ts b/GPTutor-Backend-v2/src/middleware/yookassaIpMiddleware.ts new file mode 100644 index 00000000..282358d3 --- /dev/null +++ b/GPTutor-Backend-v2/src/middleware/yookassaIpMiddleware.ts @@ -0,0 +1,90 @@ +import { FastifyRequest, FastifyReply } from "fastify"; +import { logger } from "../services/LoggerService"; + +// Список разрешенных IP адресов ЮКассы +const YOOKASSA_IPS = [ + "185.71.76.0/27", + "185.71.77.0/27", + "77.75.153.0/25", + "77.75.156.11/32", + "77.75.156.35/32", + "77.75.154.128/25", + "2a02:5180::/32", +]; + +// Функция для проверки IP в CIDR диапазоне +function ipInCIDR(ip: string, cidr: string): boolean { + // IPv6 проверка + if (ip.includes(":") && cidr.includes(":")) { + return checkIPv6InRange(ip, cidr); + } + + // IPv4 проверка + if (!cidr.includes("/")) { + return ip === cidr; + } + + const [range, bits] = cidr.split("/"); + const mask = ~(2 ** (32 - parseInt(bits)) - 1); + + const ipInt = ipToInt(ip); + const rangeInt = ipToInt(range); + + return (ipInt & mask) === (rangeInt & mask); +} + +function ipToInt(ip: string): number { + return ( + ip.split(".").reduce((int, oct) => (int << 8) + parseInt(oct), 0) >>> 0 + ); +} + +function checkIPv6InRange(ip: string, cidr: string): boolean { + // Простая проверка IPv6 по префиксу + const [prefix] = cidr.split("/"); + return ip.toLowerCase().startsWith(prefix.toLowerCase()); +} + +export function yookassaIpCheck( + request: FastifyRequest, + reply: FastifyReply, + done: Function +) { + // С trustProxy: true, request.ip уже содержит правильный IP из X-Forwarded-For + let ip = request.ip; + + // Если X-Forwarded-For содержит несколько IP (через запятую), берем первый (оригинальный клиент) + if (ip && ip.includes(",")) { + ip = ip.split(",")[0].trim(); + } + + // Удаляем ::ffff: префикс для IPv4-mapped IPv6 адресов + if (ip && ip.startsWith("::ffff:")) { + ip = ip.substring(7); + } + + logger.info("Checking IP for YooKassa webhook", { + ip, + rawIp: request.ip, + xForwardedFor: request.headers["x-forwarded-for"], + xRealIp: request.headers["x-real-ip"] + }); + + // Проверяем IP в списке разрешенных + const isAllowed = YOOKASSA_IPS.some((allowedIp) => { + try { + return ipInCIDR(String(ip), allowedIp); + } catch (error) { + logger.error("Error checking IP", error, { ip, allowedIp }); + return false; + } + }); + + if (!isAllowed) { + logger.warn("Unauthorized webhook request from IP", { ip }); + reply.code(403).send({ error: "Forbidden - Invalid IP address" }); + return; + } + + done(); +} diff --git a/GPTutor-Backend-v2/src/repositories/FileRepository.ts b/GPTutor-Backend-v2/src/repositories/FileRepository.ts new file mode 100644 index 00000000..8ad3b3c7 --- /dev/null +++ b/GPTutor-Backend-v2/src/repositories/FileRepository.ts @@ -0,0 +1,162 @@ +import { PrismaClient } from "@prisma/client"; + +export class FileRepository { + private prisma: PrismaClient; + + constructor(prisma: PrismaClient) { + this.prisma = prisma; + } + + async create(data: { + userId: string; + type: string; + name: string; + url: string; + size: number; + originalName?: string; + originalSize?: number; + converted?: boolean; + }) { + return this.prisma.file.create({ + data: { + userId: data.userId, + type: data.type, + name: data.name, + url: data.url, + size: data.size, + originalName: data.originalName, + originalSize: data.originalSize, + converted: data.converted || false, + }, + }); + } + + async findById(id: string) { + return this.prisma.file.findUnique({ + where: { id }, + }); + } + + async findByUserId(userId: string) { + return this.prisma.file.findMany({ + where: { userId }, + orderBy: { createdAt: "desc" }, + }); + } + + async delete(id: string): Promise { + await this.prisma.file.delete({ + where: { id }, + }); + } + + async update( + id: string, + data: { + name?: string; + type?: string; + url?: string; + size?: number; + } + ) { + return this.prisma.file.update({ + where: { id }, + data, + }); + } + + async findByUrl(url: string) { + return this.prisma.file.findFirst({ + where: { url }, + }); + } + + async getFileStats(userId: string): Promise<{ + totalFiles: number; + totalSize: number; + filesByType: Record; + }> { + const files = await this.prisma.file.findMany({ + where: { userId }, + select: { + type: true, + size: true, + }, + }); + + const totalFiles = files.length; + const totalSize = files.reduce( + (sum: number, file: any) => sum + file.size, + 0 + ); + + const filesByType = files.reduce( + (acc: Record, file: any) => { + const type = file.type.split("/")[0]; // Получаем основной тип (image, application, etc.) + acc[type] = (acc[type] || 0) + 1; + return acc; + }, + {} as Record + ); + + return { + totalFiles, + totalSize, + filesByType, + }; + } + + /** + * Находит файлы старше указанной даты + */ + async findOlderThan(date: Date) { + return this.prisma.file.findMany({ + where: { + createdAt: { + lt: date, + }, + }, + }); + } + + /** + * Находит файл по названию и размеру + */ + async findByNameAndSize(name: string, size: number) { + return this.prisma.file.findFirst({ + where: { + name, + size, + }, + orderBy: { + createdAt: "desc", + }, + }); + } + + /** + * Находит файл по оригинальному названию и размеру + * Используется для поиска конвертированных файлов по их оригинальным данным + */ + async findByOriginalNameAndSize(originalName: string, originalSize: number) { + return this.prisma.file.findFirst({ + where: { + originalName, + originalSize, + converted: true, + }, + orderBy: { + createdAt: "desc", + }, + }); + } + + async findByNameAndSizeOrOriginal(name: string, size: number) { + const byCurrentData = await this.findByNameAndSize(name, size); + if (byCurrentData) { + return byCurrentData; + } + + return await this.findByOriginalNameAndSize(name, size); + } +} diff --git a/GPTutor-Backend-v2/src/repositories/PaymentRepository.ts b/GPTutor-Backend-v2/src/repositories/PaymentRepository.ts new file mode 100644 index 00000000..19da80ab --- /dev/null +++ b/GPTutor-Backend-v2/src/repositories/PaymentRepository.ts @@ -0,0 +1,45 @@ +import { PrismaClient, Payment } from "@prisma/client"; + +export class PaymentRepository { + constructor(private prisma: PrismaClient) {} + + async create(data: { + userId: string; + yookassaId: string; + amount: number; + currency?: string; + description?: string; + confirmationUrl?: string; + status?: string; + }): Promise { + return await this.prisma.payment.create({ + data, + }); + } + + async findByYookassaId(yookassaId: string): Promise { + return await this.prisma.payment.findUnique({ + where: { yookassaId }, + }); + } + + async findById(id: string): Promise { + return await this.prisma.payment.findUnique({ + where: { id }, + }); + } + + async updateStatus(yookassaId: string, status: string): Promise { + return await this.prisma.payment.update({ + where: { yookassaId }, + data: { status }, + }); + } + + async findByUserId(userId: string): Promise { + return await this.prisma.payment.findMany({ + where: { userId }, + orderBy: { createdAt: "desc" }, + }); + } +} diff --git a/GPTutor-Backend-v2/src/repositories/UserRepository.ts b/GPTutor-Backend-v2/src/repositories/UserRepository.ts new file mode 100644 index 00000000..309b86f8 --- /dev/null +++ b/GPTutor-Backend-v2/src/repositories/UserRepository.ts @@ -0,0 +1,84 @@ +import { PrismaClient, User } from "@prisma/client"; +import { randomBytes } from "crypto"; + +export class UserRepository { + private prisma: PrismaClient; + + constructor(prisma: PrismaClient) { + this.prisma = prisma; + } + + private generateApiKey(): string { + const chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let result = "sk-"; + + for (let i = 0; i < 48; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + + return result; + } + + async findByVkId(vkId: string): Promise { + return this.prisma.user.findUnique({ + where: { vkId }, + }); + } + + async findById(userId: string): Promise { + return this.prisma.user.findUnique({ + where: { id: userId }, + }); + } + + async create(data: { + vkId: string; + balance?: number; + isActive?: boolean; + }): Promise { + return this.prisma.user.create({ + data: { + vkId: data.vkId, + balance: data.balance, + isActive: data.isActive ?? true, + apiKey: this.generateApiKey(), + }, + }); + } + + async findByApiKey(apiKey: string): Promise { + return this.prisma.user.findUnique({ + where: { apiKey }, + }); + } + + async updateBalance(userId: string, newBalance: number): Promise { + return this.prisma.user.update({ + where: { id: userId }, + data: { balance: newBalance }, + }); + } + + async decreaseBalance(userId: string, amount: number): Promise { + const user = await this.prisma.user.findUnique({ where: { id: userId } }); + if (!user) { + throw new Error("User not found"); + } + + const newBalance = user.balance - amount; + if (newBalance < 0) { + throw new Error("Insufficient balance"); + } + + return this.updateBalance(userId, newBalance); + } + + async updateApiKey(userId: string): Promise { + const newApiKey = this.generateApiKey(); + return this.prisma.user.update({ + where: { id: userId }, + data: { apiKey: newApiKey }, + }); + } +} diff --git a/GPTutor-Backend-v2/src/services/AuthService.ts b/GPTutor-Backend-v2/src/services/AuthService.ts new file mode 100644 index 00000000..98ab2a92 --- /dev/null +++ b/GPTutor-Backend-v2/src/services/AuthService.ts @@ -0,0 +1,58 @@ +import { + validateVKSignature, + extractVKUserData, + VKUserData, +} from "../utils/vkAuth"; +import { UserRepository } from "../repositories/UserRepository"; +import { User } from "@prisma/client"; + +export class AuthService { + private userRepository: UserRepository; + private vkAppId: string; + private vkSecretKey: string; + + constructor( + userRepository: UserRepository, + vkAppId: string, + vkSecretKey: string + ) { + this.userRepository = userRepository; + this.vkAppId = vkAppId; + this.vkSecretKey = vkSecretKey; + } + + async authorizeVKUser( + token: string + ): Promise<{ vkData: VKUserData; dbUser: User }> { + if (!validateVKSignature(token, this.vkSecretKey)) { + throw new Error("Invalid VK signature"); + } + + const userData = extractVKUserData(token); + if (!userData || !userData.vk_user_id) { + throw new Error("Invalid VK user data"); + } + + console.log(userData.vk_app_id); + console.log(this.vkAppId); + + if (userData.vk_app_id !== this.vkAppId) { + throw new Error("Invalid VK app ID"); + } + + let user = await this.userRepository.findByVkId(userData.vk_user_id); + + if (!user) { + user = await this.userRepository.create({ vkId: userData.vk_user_id }); + console.log(`Created new user with VK ID: ${userData.vk_user_id}`); + } + + return { vkData: userData, dbUser: user }; + } + + async updateUserToken(userId: string): Promise { + const user = await this.userRepository.updateApiKey(userId); + console.log(`Updated API key for user: ${userId}`); + return user; + } +} diff --git a/GPTutor-Backend-v2/src/services/FileCleanupService.ts b/GPTutor-Backend-v2/src/services/FileCleanupService.ts new file mode 100644 index 00000000..6556f18a --- /dev/null +++ b/GPTutor-Backend-v2/src/services/FileCleanupService.ts @@ -0,0 +1,150 @@ +import { PrismaClient } from "@prisma/client"; +import { logger } from "./LoggerService"; +import { FilesService } from "./FilesService"; + +/** + * Сервис для автоматической очистки старых файлов + */ +export class FileCleanupService { + private prisma: PrismaClient; + private filesService: FilesService; + private cleanupInterval: NodeJS.Timeout | null = null; + private readonly FILE_LIFETIME_HOURS = 20; + private readonly CLEANUP_INTERVAL_MS = 60 * 60 * 1000; + + constructor(prisma: PrismaClient, filesService: FilesService) { + this.prisma = prisma; + this.filesService = filesService; + } + + /** + * Запускает автоматическую очистку файлов + */ + start(): void { + logger.info("Starting file cleanup service", { + lifetimeHours: this.FILE_LIFETIME_HOURS, + checkIntervalMinutes: this.CLEANUP_INTERVAL_MS / (60 * 1000), + }); + + this.cleanupOldFiles(); + + this.cleanupInterval = setInterval(() => { + this.cleanupOldFiles(); + }, this.CLEANUP_INTERVAL_MS); + } + + /** + * Останавливает автоматическую очистку файлов + */ + stop(): void { + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval); + this.cleanupInterval = null; + logger.info("File cleanup service stopped"); + } + } + + /** + * Выполняет очистку старых файлов + */ + private async cleanupOldFiles(): Promise { + try { + const cutoffDate = new Date(); + cutoffDate.setHours(cutoffDate.getHours() - this.FILE_LIFETIME_HOURS); + + logger.info("Starting file cleanup", { + cutoffDate: cutoffDate.toISOString(), + lifetimeHours: this.FILE_LIFETIME_HOURS, + }); + + const oldFiles = await this.prisma.file.findMany({ + where: { + createdAt: { + lt: cutoffDate, + }, + }, + }); + + if (oldFiles.length === 0) { + logger.info("No old files to cleanup"); + return; + } + + logger.info(`Found ${oldFiles.length} old files to cleanup`, { + fileIds: oldFiles.map((f) => f.id), + }); + + let successCount = 0; + let errorCount = 0; + + // Удаляем каждый файл + for (const file of oldFiles) { + try { + // Удаляем только запись из БД (файл остается в S3) + await this.prisma.file.delete({ + where: { id: file.id }, + }); + + successCount++; + + logger.info("File cleaned up successfully", { + fileId: file.id, + fileName: file.name, + userId: file.userId, + age: this.getFileAge(file.createdAt), + }); + } catch (error) { + errorCount++; + logger.error("Failed to cleanup file", error, { + fileId: file.id, + fileName: file.name, + url: file.url, + }); + } + } + + logger.info("File cleanup completed", { + totalFiles: oldFiles.length, + successCount, + errorCount, + cutoffDate: cutoffDate.toISOString(), + }); + } catch (error) { + logger.error("File cleanup failed", error); + } + } + + /** + * Вычисляет возраст файла в часах + */ + private getFileAge(createdAt: Date): string { + const now = new Date(); + const ageMs = now.getTime() - createdAt.getTime(); + const ageHours = Math.floor(ageMs / (1000 * 60 * 60)); + const ageMinutes = Math.floor((ageMs % (1000 * 60 * 60)) / (1000 * 60)); + return `${ageHours}h ${ageMinutes}m`; + } + + /** + * Ручная очистка старых файлов (для вызова из API или CLI) + */ + async cleanupNow(): Promise<{ + success: boolean; + message: string; + stats?: { total: number; success: number; errors: number }; + }> { + try { + await this.cleanupOldFiles(); + return { + success: true, + message: "Cleanup completed successfully", + }; + } catch (error) { + logger.error("Manual cleanup failed", error); + return { + success: false, + message: "Cleanup failed", + }; + } + } +} diff --git a/GPTutor-Backend-v2/src/services/FilesService.ts b/GPTutor-Backend-v2/src/services/FilesService.ts new file mode 100644 index 00000000..f955480f --- /dev/null +++ b/GPTutor-Backend-v2/src/services/FilesService.ts @@ -0,0 +1,328 @@ +import sharp from "sharp"; +import { compress } from "compress-pdf"; +//@ts-ignore +import EasyYandexS3 from "easy-yandex-s3"; +import crypto from "crypto"; +import { S3 } from "aws-sdk"; +import { logger } from "./LoggerService"; +//@ts-ignore +import libre from "libreoffice-convert"; +import { promisify } from "util"; + +// Используем extend для callback, чтобы избежать deprecation warning +const libreConvert = promisify(libre.convert.bind(libre)); + +const s3 = new EasyYandexS3({ + auth: { + accessKeyId: process.env.YANDEX_ACCESS_KEY_ID!, + secretAccessKey: process.env.YANDEX_SECRET_ACCESS_KEY!, + }, + httpOptions: { + timeout: 60000, + }, + Bucket: process.env.YANDEX_BUCKET!, + debug: process.env.NODE_ENV === "development", +}); + +export class FilesService { + private getExtension(fileName: string): string { + const splitFileName = fileName.split("."); + return splitFileName[splitFileName.length - 1].toLowerCase(); + } + + private getFileWithExtension(name: string, originalFileName: string): string { + return `${name}.${this.getExtension(originalFileName)}`; + } + + /** + * Проверяет, нужна ли конвертация файла в PDF + */ + private needsConversionToPdf(fileName: string): boolean { + const extension = this.getExtension(fileName); + const convertibleExtensions = ["doc", "docx", "ppt", "pptx"]; + return convertibleExtensions.includes(extension); + } + + /** + * Конвертирует документ в PDF с помощью LibreOffice (через libreoffice-convert) + * Работает напрямую с буферами без создания временных файлов + */ + private async convertToPdf( + buffer: Buffer, + originalFileName: string + ): Promise<{ buffer: Buffer; newFileName: string }> { + const startTime = Date.now(); + + try { + logger.info("Converting document to PDF using libreoffice-convert", { + fileName: originalFileName, + fileSize: buffer.length, + }); + + // Определяем расширение выходного файла + const ext = ".pdf"; + + // Конвертируем буфер напрямую в PDF + // @ts-ignore - libreoffice-convert не имеет типов TypeScript + const pdfBuffer: Buffer = await libreConvert(buffer, ext, undefined); + + const duration = Date.now() - startTime; + + // Проверяем результат + if (!pdfBuffer || pdfBuffer.length === 0) { + throw new Error("Conversion resulted in empty buffer"); + } + + // Формируем финальное имя файла + const baseFileName = originalFileName.substring( + 0, + originalFileName.lastIndexOf(".") + ); + const finalPdfFileName = `${baseFileName}.pdf`; + + logger.info("Document converted to PDF successfully", { + originalFileName, + finalPdfFileName, + originalSize: buffer.length, + pdfSize: pdfBuffer.length, + durationMs: duration, + }); + + return { + buffer: pdfBuffer, + newFileName: finalPdfFileName, + }; + } catch (error) { + const duration = Date.now() - startTime; + const errorMessage = (error as Error).message; + + logger.error("Failed to convert document to PDF", error, { + fileName: originalFileName, + durationMs: duration, + errorMessage, + }); + + // Определяем тип ошибки для более понятного сообщения + if ( + errorMessage.includes( + "Could not find platform independent libraries" + ) || + errorMessage.includes("soffice") || + errorMessage.includes("LibreOffice") + ) { + throw new Error( + "LibreOffice not found. Please install LibreOffice on your system. " + + "Visit: https://www.libreoffice.org/download/" + ); + } + + if (errorMessage.includes("Document is empty")) { + throw new Error( + "Failed to read document. The file may be corrupted or in an unsupported format." + ); + } + + throw new Error(`Failed to convert document to PDF: ${errorMessage}`); + } + } + + private async optimizePhotos( + arrayBuffer: ArrayBuffer, + fileName: string + ): Promise { + try { + let extension = this.getExtension(fileName); + + if (extension === "jpg") { + extension = "jpeg"; + } + + const createdSharp = sharp(arrayBuffer); + if (extension in createdSharp) { + // @ts-ignore + return await createdSharp[extension]({ quality: 60 }).toBuffer(); + } + + return Buffer.from(arrayBuffer); + } catch (error) { + console.log("Error optimizing photo:", error); + return Buffer.from(arrayBuffer); + } + } + + private async optimizeAttachment( + arrayBuffer: ArrayBuffer, + fileName: string + ): Promise { + const typeFile = this.determineFileType(fileName); + const extension = this.getExtension(fileName); + + logger.info("TypeFile", typeFile); + + // Конвертируем doc/docx/ppt/pptx в PDF + if (this.needsConversionToPdf(fileName)) { + logger.info("Document needs conversion to PDF", { fileName, extension }); + const buffer = Buffer.from(arrayBuffer); + const { buffer: pdfBuffer } = await this.convertToPdf(buffer, fileName); + + // Оптимизируем полученный PDF + logger.info("Optimizing converted PDF"); + return await compress(pdfBuffer); + } + + // if (typeFile === "photo") { + // return await this.optimizePhotos(arrayBuffer, fileName); + // } + + if (typeFile === "text") { + return Buffer.from(arrayBuffer).toString("utf-8"); + } + + if (extension === "pdf") { + return await compress(Buffer.from(arrayBuffer)); + } + + return Buffer.from(arrayBuffer); + } + + private determineFileType(filename: string): "photo" | "document" | "text" { + if (filename.length === 0) { + throw new Error("Invalid filename: Must be a non-empty string."); + } + + const photoExtensions: string[] = [ + "jpg", + "jpeg", + "png", + "gif", + "bmp", + "svg", + "webp", + "tiff", + "tif", + ]; + + const documentExtensions: string[] = [ + "pdf", + "doc", + "docx", + "xls", + "xlsx", + "csv", + "ppt", + "pptx", + ]; + + const textExtensions: string[] = [ + "txt", + "js", + "html", + "css", + "json", + "xml", + "md", + "log", + "py", + "java", + "c", + "cpp", + "h", + "sh", + "config", + "conf", + "ini", + "yml", + "yaml", + "sql", + ]; + + const dotIndex: number = filename.lastIndexOf("."); + + if (dotIndex === -1 || dotIndex === filename.length - 1) { + throw new Error(`Unknown file type: '${filename}' has no extension.`); + } + + const extension: string = filename.slice(dotIndex + 1).toLowerCase(); + + if (photoExtensions.includes(extension)) { + return "photo"; + } else if (documentExtensions.includes(extension)) { + return "document"; + } else if (textExtensions.includes(extension)) { + return "text"; + } else { + throw new Error( + `Unknown or unsupported file type with extension '.${extension}'.` + ); + } + } + + async uploadFile( + arrayBuffer: ArrayBuffer | string | Buffer | Uint8Array, + name: string + ): Promise { + console.log(arrayBuffer); + logger.info("File Name", { + name: this.getFileWithExtension(crypto.randomUUID(), name), + }); + return (await s3.Upload( + { + //@ts-ignore + buffer: arrayBuffer, + name: this.getFileWithExtension(crypto.randomUUID(), name), + }, + "/" + )) as S3.ManagedUpload.SendData; + } + + async optimizeAndUploadFile( + arrayBuffer: ArrayBuffer, + fileName: string + ): Promise<{ + url: string; + optimizedData: ArrayBuffer | Buffer | string | Uint8Array; + finalFileName: string; + }> { + let finalFileName = fileName; + + console.log( + "this.needsConversionToPdf(fileName)", + this.needsConversionToPdf(fileName) + ); + if (this.needsConversionToPdf(fileName)) { + const baseFileName = fileName.substring(0, fileName.lastIndexOf(".")); + finalFileName = `${baseFileName}.pdf`; + } + + const optimizedData = await this.optimizeAttachment(arrayBuffer, fileName); + const uploadResult = await this.uploadFile(optimizedData, finalFileName); + console.log(uploadResult); + logger.info("UploadResult", uploadResult); + + return { + url: uploadResult.Location, + optimizedData, + finalFileName, + }; + } + + /** + * Удаляет файл из S3 по URL + */ + async deleteFile(fileUrl: string): Promise { + try { + // Извлекаем имя файла из URL + const url = new URL(fileUrl); + const fileName = url.pathname.substring(1); // Убираем первый слеш + + logger.info("Deleting file from S3", { fileName, fileUrl }); + + await s3.Remove(fileName); + + logger.info("File deleted from S3 successfully", { fileName }); + } catch (error) { + logger.error("Failed to delete file from S3", error, { fileUrl }); + throw error; + } + } +} diff --git a/GPTutor-Backend-v2/src/services/LLMCostEvaluate.ts b/GPTutor-Backend-v2/src/services/LLMCostEvaluate.ts new file mode 100644 index 00000000..64b76a4f --- /dev/null +++ b/GPTutor-Backend-v2/src/services/LLMCostEvaluate.ts @@ -0,0 +1,192 @@ +import { + OpenRouterModel, + OpenRouterApiResponse, + CostCalculation, + UsageParams, +} from "../types/openrouter"; + +interface ExchangeRateResponse { + rates: { + [key: string]: number; + }; + base: string; + date: string; +} + +export class LLMCostEvaluate { + private models: OpenRouterModel[] = []; + private usdToRubRate: number; + private readonly OPENROUTER_API_URL = "https://openrouter.ai/api/v1/models"; + private readonly EXCHANGE_RATE_API_URL = + "https://api.exchangerate-api.com/v4/latest/USD"; + private readonly UPDATE_INTERVAL_MS = 60 * 60 * 1000; + private updateIntervalId?: NodeJS.Timeout; + + constructor(usdToRubRate: number = 100) { + this.usdToRubRate = usdToRubRate; + } + + private async fetchExchangeRate(): Promise { + try { + console.log("🔄 Fetching USD to RUB exchange rate..."); + + const response = await fetch(this.EXCHANGE_RATE_API_URL); + if (!response.ok) { + throw new Error( + `Failed to fetch exchange rate: ${response.status} ${response.statusText}` + ); + } + + const data = (await response.json()) as ExchangeRateResponse; + const rate = data.rates?.RUB; + + if (rate && typeof rate === "number") { + const rateWithMarkup = rate * 1.17; + this.usdToRubRate = rateWithMarkup; + console.log( + `✅ Updated USD to RUB rate: ${rate.toFixed( + 2 + )} → ${rateWithMarkup.toFixed(2)} (with 17% markup)` + ); + } else { + throw new Error("Invalid exchange rate data received"); + } + } catch (error) { + console.error( + "❌ Failed to fetch exchange rate, using fallback value 100:", + error + ); + this.usdToRubRate = 100; + } + } + + private startExchangeRateUpdates(): void { + if (this.updateIntervalId) { + clearInterval(this.updateIntervalId); + } + + this.updateIntervalId = setInterval(() => { + this.fetchExchangeRate(); + }, this.UPDATE_INTERVAL_MS); + + console.log( + `⏰ Exchange rate will update every ${ + this.UPDATE_INTERVAL_MS / 1000 / 60 + } minutes` + ); + } + + async initialize(): Promise { + try { + console.log("🔄 Loading OpenRouter Models..."); + + await this.fetchExchangeRate(); + + this.startExchangeRateUpdates(); + + const response = await fetch(this.OPENROUTER_API_URL); + if (!response.ok) { + throw new Error( + `Failed to fetch models: ${response.status} ${response.statusText}` + ); + } + + const data = (await response.json()) as OpenRouterApiResponse; + this.models = data.data; + + console.log(`✅ Loaded ${this.models.length} OpenRouter models`); + } catch (error) { + console.error("❌ Failed to load OpenRouter Models:", error); + throw error; + } + } + + getAllModels(): OpenRouterModel[] { + return this.models; + } + + calculateCost(cost: number): number { + return cost * this.usdToRubRate; + } + + getModelsWithRubPricing(searchTerm?: string, provider?: string): any[] { + let filteredModels = this.models; + + if (searchTerm) { + const term = searchTerm.toLowerCase(); + filteredModels = filteredModels.filter( + (model) => + model.name.toLowerCase().includes(term) || + model.id.toLowerCase().includes(term) || + model.description.toLowerCase().includes(term) + ); + } + + if (provider) { + filteredModels = filteredModels.filter((model) => + model.id.toLowerCase().includes(provider.toLowerCase()) + ); + } + + return filteredModels.map((model) => ({ + ...model, + pricing: { + prompt: this.calculateCost(parseFloat(model.pricing.prompt)), + completion: this.calculateCost(parseFloat(model.pricing.completion)), + request: this.calculateCost(parseFloat(model.pricing.request)), + image: this.calculateCost(parseFloat(model.pricing.image)), + web_search: this.calculateCost(parseFloat(model.pricing.web_search)), + internal_reasoning: this.calculateCost( + parseFloat(model.pricing.internal_reasoning) + ), + input_cache_read: model.pricing.input_cache_read + ? this.calculateCost(parseFloat(model.pricing.input_cache_read)) + : undefined, + input_cache_write: model.pricing.input_cache_write + ? this.calculateCost(parseFloat(model.pricing.input_cache_write)) + : undefined, + }, + })); + } + + getModelsByProviders(providers: string[]): any[] { + return this.getModelsWithRubPricing() + .filter((model) => + providers.some((provider) => + model.id.toLowerCase().startsWith(`${provider.toLowerCase()}/`) + ) + ) + .filter((model) => model.id !== "openai/o1-pro"); + } + + getPopularProviderModels(): any[] { + const popularProviders = [ + "x-ai", + "deepseek", + "google", + "qwen", + "perplexity", + "mistralai", + "openai", + "anthropic", + ]; + + return this.getModelsByProviders(popularProviders).filter( + (model) => !model.id.toLowerCase().includes(":free") + ); + } + + // Метод для остановки обновлений курса (для корректной очистки ресурсов) + stopExchangeRateUpdates(): void { + if (this.updateIntervalId) { + clearInterval(this.updateIntervalId); + this.updateIntervalId = undefined; + console.log("🛑 Exchange rate updates stopped"); + } + } + + // Геттер для получения текущего курса + getCurrentExchangeRate(): number { + return this.usdToRubRate; + } +} diff --git a/GPTutor-Backend-v2/src/services/LoggerService.ts b/GPTutor-Backend-v2/src/services/LoggerService.ts new file mode 100644 index 00000000..449c310d --- /dev/null +++ b/GPTutor-Backend-v2/src/services/LoggerService.ts @@ -0,0 +1,272 @@ +import winston from "winston"; +import DailyRotateFile from "winston-daily-rotate-file"; +import path from "path"; + +export class LoggerService { + private logger: winston.Logger; + private static instance: LoggerService; + + // Safe JSON stringify that handles circular references + private static safeStringify(obj: any, space?: number): string { + const seen = new WeakSet(); + return JSON.stringify(obj, (key, val) => { + if (val != null && typeof val === "object") { + if (seen.has(val)) { + return "[Circular]"; + } + seen.add(val); + } + return val; + }, space); + } + + private constructor() { + this.logger = this.createLogger(); + } + + public static getInstance(): LoggerService { + if (!LoggerService.instance) { + LoggerService.instance = new LoggerService(); + } + return LoggerService.instance; + } + + private createLogger(): winston.Logger { + const logDir = path.join(process.cwd(), "logs"); + + // Custom format for better readability + const customFormat = winston.format.combine( + winston.format.timestamp({ + format: "YYYY-MM-DD HH:mm:ss", + }), + winston.format.errors({ stack: true }), + winston.format.printf( + ({ + timestamp, + level, + message, + service, + userId, + requestId, + ...meta + }) => { + let logMessage = `[${timestamp}] ${level.toUpperCase()}`; + + if (service) logMessage += ` [${service}]`; + if (requestId) logMessage += ` [ReqID: ${requestId}]`; + if (userId) logMessage += ` [User: ${userId}]`; + + logMessage += `: ${message}`; + + const metaStr = Object.keys(meta).length + ? ` ${LoggerService.safeStringify(meta)}` + : ""; + return logMessage + metaStr; + } + ) + ); + + const consoleFormat = winston.format.combine( + winston.format.colorize(), + customFormat + ); + + return winston.createLogger({ + level: process.env.LOG_LEVEL || "info", + format: customFormat, + defaultMeta: { service: "gptutor-backend" }, + transports: [ + new winston.transports.Console({ + format: consoleFormat, + }), + + new DailyRotateFile({ + filename: path.join(logDir, "error-%DATE%.log"), + datePattern: "YYYY-MM-DD", + level: "error", + maxSize: "20m", + maxFiles: "14d", + format: winston.format.combine( + winston.format.timestamp(), + winston.format.json() + ), + }), + + new DailyRotateFile({ + filename: path.join(logDir, "combined-%DATE%.log"), + datePattern: "YYYY-MM-DD", + maxSize: "20m", + maxFiles: "30d", + format: winston.format.combine( + winston.format.timestamp(), + winston.format.json() + ), + }), + + // API requests log file + new DailyRotateFile({ + filename: path.join(logDir, "api-%DATE%.log"), + datePattern: "YYYY-MM-DD", + maxSize: "20m", + maxFiles: "30d", + level: "info", + format: winston.format.combine( + winston.format.timestamp(), + winston.format.json() + ), + }), + ], + }); + } + + // Basic logging methods + public debug(message: string, meta?: any): void { + this.logger.debug(message, meta); + } + + public info(message: string, meta?: any): void { + this.logger.info(message, meta); + } + + public warn(message: string, meta?: any): void { + this.logger.warn(message, meta); + } + + public error(message: string, error?: Error | any, meta?: any): void { + if (error instanceof Error) { + this.logger.error(message, { + error: error.message, + stack: error.stack, + ...meta, + }); + } else { + this.logger.error(message, { error, ...meta }); + } + } + + // Specialized logging methods + public apiRequest( + method: string, + url: string, + userId?: string, + meta?: any + ): void { + this.logger.info(`${method} ${url}`, { + type: "api", + service: "api", + method, + url, + userId, + ...meta, + }); + } + + public apiResponse( + method: string, + url: string, + statusCode: number, + duration: number, + userId?: string, + meta?: any + ): void { + this.logger.info(`${method} ${url} - ${statusCode} (${duration}ms)`, { + type: "api", + service: "api", + method, + url, + statusCode, + duration, + userId, + ...meta, + }); + } + + public llmRequest( + model: string, + userId: string, + tokens?: number, + cost?: number, + meta?: any + ): void { + this.logger.info(`LLM Request: ${model}`, { + type: "llm", + service: "llm", + model, + userId, + tokens, + cost, + ...meta, + }); + } + + public llmResponse( + model: string, + userId: string, + tokens: number, + cost: number, + duration: number, + meta?: any + ): void { + this.logger.info( + `LLM Response: ${model} - ${tokens} tokens, ${cost} RUB (${duration}ms)`, + { + type: "llm", + service: "llm", + model, + userId, + tokens, + cost, + duration, + ...meta, + } + ); + } + + public auth( + action: string, + userId?: string, + success: boolean = true, + meta?: any + ): void { + const level = success ? "info" : "warn"; + this.logger[level](`Auth ${action}: ${success ? "SUCCESS" : "FAILED"}`, { + type: "auth", + service: "auth", + action, + userId, + success, + ...meta, + }); + } + + public balance( + action: string, + userId: string, + amount: number, + newBalance: number, + meta?: any + ): void { + this.logger.info(`Balance ${action}: ${amount} RUB`, { + type: "balance", + service: "balance", + action, + userId, + amount, + newBalance, + ...meta, + }); + } + + // Create child logger with additional context + public child(defaultMeta: any): winston.Logger { + return this.logger.child(defaultMeta); + } + + // Get the underlying winston logger + public getWinstonLogger(): winston.Logger { + return this.logger; + } +} + +// Export singleton instance +export const logger = LoggerService.getInstance(); diff --git a/GPTutor-Backend-v2/src/services/OpenRouterService.ts b/GPTutor-Backend-v2/src/services/OpenRouterService.ts new file mode 100644 index 00000000..bfef2929 --- /dev/null +++ b/GPTutor-Backend-v2/src/services/OpenRouterService.ts @@ -0,0 +1,62 @@ +import OpenAI from "openai"; +import type { + ChatCompletion, + ChatCompletionCreateParams, +} from "openai/resources/chat/completions"; + +export class OpenRouterService { + private client: OpenAI; + + constructor(apiKey: string) { + this.client = new OpenAI({ + apiKey: apiKey, + baseURL: "https://openrouter.ai/api/v1", + defaultHeaders: { + "HTTP-Referer": "http://localhost:3001", + "X-Title": "GPTutor API v2", + }, + }); + } + + async createCompletion( + params: ChatCompletionCreateParams + ): Promise { + try { + const completion = await this.client.chat.completions.create({ + ...params, + stream: false, + usage: { include: true }, + } as any); + return completion as ChatCompletion; + } catch (error) { + console.error("OpenRouter API request failed:", error); + throw error; + } + } + + async createCompletionStream(params: ChatCompletionCreateParams) { + try { + //@ts-ignore + return this.client.chat.completions.create({ + ...params, + stream: true, + usage: { + include: true, + }, + }); + } catch (error) { + console.error("OpenRouter API stream request failed:", error); + throw error; + } + } + + async getModels() { + try { + const models = await this.client.models.list(); + return models; + } catch (error) { + console.error("Failed to get Models from OpenRouter:", error); + throw error; + } + } +} diff --git a/GPTutor-Backend-v2/src/services/YooKassaService.ts b/GPTutor-Backend-v2/src/services/YooKassaService.ts new file mode 100644 index 00000000..ed1f861c --- /dev/null +++ b/GPTutor-Backend-v2/src/services/YooKassaService.ts @@ -0,0 +1,162 @@ +import { YooCheckout, ICreatePayment } from '@a2seven/yoo-checkout'; +import { PaymentRepository } from '../repositories/PaymentRepository'; +import { UserRepository } from '../repositories/UserRepository'; +import { logger } from './LoggerService'; + +export interface CreatePaymentParams { + userId: string; + amount: number; + description?: string; + returnUrl?: string; +} + +export class YooKassaService { + private yooCheckout: YooCheckout; + + constructor( + private shopId: string, + private secretKey: string, + private paymentRepository: PaymentRepository, + private userRepository: UserRepository + ) { + this.yooCheckout = new YooCheckout({ + shopId: this.shopId, + secretKey: this.secretKey, + }); + + logger.info('YooKassaService initialized', { shopId }); + } + + async createPayment(params: CreatePaymentParams) { + const { userId, amount, description, returnUrl } = params; + + logger.info('Creating payment', { userId, amount }); + + try { + const idempotenceKey = `${userId}-${Date.now()}-${Math.random()}`; + + const createPayload: ICreatePayment = { + amount: { + value: amount.toFixed(2), + currency: 'RUB', + }, + capture: true, + confirmation: { + type: 'redirect', + return_url: returnUrl || 'https://vk.com/app54187353#/profile', + }, + description: description || `Пополнение баланса на ${amount}₽`, + }; + + const payment = await this.yooCheckout.createPayment( + createPayload, + idempotenceKey + ); + + logger.info('YooKassa payment created', { + paymentId: payment.id, + status: payment.status, + amount: payment.amount.value, + }); + + // Сохраняем платеж в БД + const dbPayment = await this.paymentRepository.create({ + userId, + yookassaId: payment.id, + amount, + currency: 'RUB', + description: createPayload.description, + confirmationUrl: payment.confirmation?.confirmation_url, + status: payment.status, + }); + + logger.info('Payment saved to database', { + dbPaymentId: dbPayment.id, + yookassaId: payment.id, + }); + + return { + id: dbPayment.id, + yookassaId: payment.id, + amount, + status: payment.status, + confirmationUrl: payment.confirmation?.confirmation_url, + createdAt: dbPayment.createdAt, + }; + } catch (error) { + logger.error('Failed to create payment', error, { userId, amount }); + throw error; + } + } + + async getPaymentInfo(paymentId: string) { + try { + const payment = await this.yooCheckout.getPayment(paymentId); + + logger.info('Payment info retrieved', { + paymentId, + status: payment.status, + }); + + return payment; + } catch (error) { + logger.error('Failed to get payment info', error, { paymentId }); + throw error; + } + } + + async handlePaymentWebhook(paymentData: any) { + try { + const { id, status, amount } = paymentData.object; + + logger.info('Processing payment webhook', { + paymentId: id, + status, + amount: amount?.value, + }); + + const dbPayment = await this.paymentRepository.findByYookassaId(id); + + if (!dbPayment) { + logger.warn('Payment not found in database', { paymentId: id }); + return { success: false, message: 'Payment not found' }; + } + + // Обновляем статус платежа + await this.paymentRepository.updateStatus(id, status); + + // Если платеж успешен, пополняем баланс пользователя + if (status === 'succeeded') { + const user = await this.userRepository.findById(dbPayment.userId); + + if (user) { + const newBalance = user.balance + dbPayment.amount; + await this.userRepository.updateBalance(dbPayment.userId, newBalance); + + logger.info('User balance updated', { + userId: dbPayment.userId, + oldBalance: user.balance, + newBalance, + addedAmount: dbPayment.amount, + }); + } + } + + return { + success: true, + status, + paymentId: id, + }; + } catch (error) { + logger.error('Failed to process payment webhook', error, { + paymentData, + }); + throw error; + } + } + + async getUserPayments(userId: string) { + return await this.paymentRepository.findByUserId(userId); + } +} + diff --git a/GPTutor-Backend-v2/src/types/openrouter.ts b/GPTutor-Backend-v2/src/types/openrouter.ts new file mode 100644 index 00000000..c3c088b7 --- /dev/null +++ b/GPTutor-Backend-v2/src/types/openrouter.ts @@ -0,0 +1,73 @@ +export interface OpenRouterPricing { + prompt: string; // Cost per input token + completion: string; // Cost per output token + request: string; // Fixed cost per API request + image: string; // Cost per image input + web_search: string; // Cost per web search operation + internal_reasoning: string; // Cost for internal reasoning tokens + input_cache_read?: string; // Cost per cached input token read + input_cache_write?: string; // Cost per cached input token write +} + +export interface OpenRouterArchitecture { + modality: string; + input_modalities: string[]; + output_modalities: string[]; + tokenizer: string; + instruct_type: string | null; +} + +export interface OpenRouterTopProvider { + context_length: number; + max_completion_tokens: number; + is_moderated: boolean; +} + +export interface OpenRouterDefaultParameters { + temperature: number | null; + top_p: number | null; + frequency_penalty: number | null; +} + +export interface OpenRouterModel { + id: string; + canonical_slug: string; + hugging_face_id: string; + name: string; + created: number; + description: string; + context_length: number; + architecture: OpenRouterArchitecture; + pricing: OpenRouterPricing; + top_provider: OpenRouterTopProvider; + per_request_limits: any; + supported_parameters: string[]; + default_parameters: OpenRouterDefaultParameters; +} + +export interface OpenRouterApiResponse { + data: OpenRouterModel[]; +} + +export interface CostCalculation { + promptCostRub: number; + completionCostRub: number; + requestCostRub: number; + imageCostRub: number; + totalCostRub: number; +} + +export interface UsageParams { + promptTokens?: number; + completionTokens?: number; + images?: number; + requests?: number; +} + + + + + + + + diff --git a/GPTutor-Backend-v2/src/utils/vkAuth.ts b/GPTutor-Backend-v2/src/utils/vkAuth.ts new file mode 100644 index 00000000..c382a3be --- /dev/null +++ b/GPTutor-Backend-v2/src/utils/vkAuth.ts @@ -0,0 +1,133 @@ +import * as crypto from "crypto"; +import { UserRepository } from "../repositories/UserRepository"; + +export function validateVKSignature( + queryString: string, + secretKey: string +): boolean { + console.log({ queryString }); + console.log({ secretKey }); + try { + // Handle both full URL and query string + let params: URLSearchParams; + + if (queryString.startsWith("http")) { + // Full URL + const url = new URL(queryString); + params = new URLSearchParams(url.search); + } else { + // Query string only + params = new URLSearchParams(queryString); + } + + const sign = params.get("sign"); + if (!sign) return false; + + params.delete("sign"); + + const sortedParams = Array.from(params.entries()) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, value]) => `${key}=${value}`) + .join("&"); + + const hmac = crypto.createHmac("sha256", secretKey); + hmac.update(sortedParams); + const computedSign = hmac.digest("base64url"); + + // Debug logging + console.log("VK Signature validation:"); + console.log("Sorted params:", sortedParams); + console.log("Received sign:", sign); + console.log("Computed sign:", computedSign); + console.log("Signatures match:", computedSign === sign); + + return computedSign === sign; + } catch (error) { + console.error("VK signature validation error:", error); + return false; + } +} + +export function extractVKUserData(queryString: string) { + try { + // Handle both full URL and query string + let params: URLSearchParams; + + if (queryString.startsWith("http")) { + // Full URL + const url = new URL(queryString); + params = new URLSearchParams(url.search); + } else { + // Query string only + params = new URLSearchParams(queryString); + } + + return { + vk_user_id: params.get("vk_user_id"), + vk_app_id: params.get("vk_app_id"), + vk_is_app_user: params.get("vk_is_app_user"), + vk_language: params.get("vk_language"), + vk_platform: params.get("vk_platform"), + vk_ts: params.get("vk_ts"), + }; + } catch (error) { + console.error("Error extracting VK user data:", error); + return null; + } +} + +export interface VKUserData { + vk_user_id: string | null; + vk_app_id: string | null; + vk_is_app_user: string | null; + vk_language: string | null; + vk_platform: string | null; + vk_ts: string | null; +} + +export async function validateApiKey( + apiKey: string, + userRepository: UserRepository +): Promise { + try { + if (!apiKey.startsWith("sk-")) { + return null; + } + + const user = await userRepository.findByApiKey(apiKey); + + if (!user || !user.isActive) { + return null; + } + + return user; + } catch (error) { + console.error("API key validation error:", error); + return null; + } +} + +export async function authenticateUser( + authHeader: string | undefined, + vkSecretKey: string, + userRepository: UserRepository +): Promise { + if (authHeader && authHeader.startsWith("Bearer ")) { + const apiKey = authHeader.substring(7); + if (apiKey.startsWith("sk-")) { + const user = await validateApiKey(apiKey, userRepository); + if (user) { + return { user, authType: "api_key" }; + } + } + + if (validateVKSignature(apiKey, vkSecretKey)) { + const vkData = extractVKUserData(apiKey); + if (vkData && vkData.vk_user_id) { + return { user: vkData, authType: "vk" }; + } + } + } + + return null; +} diff --git a/GPTutor-Backend-v2/tsconfig.json b/GPTutor-Backend-v2/tsconfig.json new file mode 100644 index 00000000..b7354da4 --- /dev/null +++ b/GPTutor-Backend-v2/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020"], + "module": "commonjs", + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "./src", + "resolveJsonModule": true, + "declaration": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} + diff --git a/GPTutor-Backend/src/main/java/com/chatgpt/services/ConversationsService.java b/GPTutor-Backend/src/main/java/com/chatgpt/services/ConversationsService.java index d4653032..745a18e5 100644 --- a/GPTutor-Backend/src/main/java/com/chatgpt/services/ConversationsService.java +++ b/GPTutor-Backend/src/main/java/com/chatgpt/services/ConversationsService.java @@ -31,11 +31,11 @@ public class ConversationsService { @Value("${rag.url}") String ragUrl; - @Value("${master-token}") - String masterToken; +// @Value("${master-token}") +// String masterToken; - @Value("${deep-url}") - String deepUrl; +// @Value("${deep-url}") +// String deepUrl; @Autowired ApiRequestsService apiRequestsService; @@ -70,7 +70,7 @@ public void fetchCompletion(Utf8SseEmitter emitter, ConversationRequest conversa String input = mapper.writeValueAsString(chatGptRequest); HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(deepUrl + "/v1/chat/completions")) + .uri(URI.create("/v1/chat/completions")) .header("Content-Type", "application/json") .header("Authorization", "Bearer " + apiKey) .POST(HttpRequest.BodyPublishers.ofString(input)) diff --git a/GPTutor-Backend/src/main/java/com/chatgpt/services/DeepService.java b/GPTutor-Backend/src/main/java/com/chatgpt/services/DeepService.java index 7a019314..9f631772 100644 --- a/GPTutor-Backend/src/main/java/com/chatgpt/services/DeepService.java +++ b/GPTutor-Backend/src/main/java/com/chatgpt/services/DeepService.java @@ -12,12 +12,12 @@ @Service public class DeepService { - @Value("${master-token}") - String masterToken; +// @Value("${master-token}") +// String masterToken; - @Value("${deep-url}") - String deepUrl; +// @Value("${deep-url}") +// String deepUrl; public String getUserId() { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); @@ -33,7 +33,7 @@ public String getUserId() { public void updateUserToken(String operation, int amount) { String userId = this.getUserId(); - String url = String.format(deepUrl + "/token?userId=%s&masterToken=%s", userId, this.masterToken); + String url = String.format( "/token?userId=%s&masterToken=%s", userId, ""); System.out.println(url); RestTemplate restTemplate = new RestTemplate(); @@ -52,7 +52,7 @@ public String getUserToken() { String userId = this.getUserId(); System.out.println(userId); - String url = String.format(deepUrl + "/token?userId=%s&masterToken=%s", userId, this.masterToken); + String url = String.format("/token?userId=%s&masterToken=%s", userId, ""); System.out.println(url); RestTemplate restTemplate = new RestTemplate(); @@ -64,7 +64,7 @@ public String getUserToken() { public boolean hasUser() { String userId = this.getUserId(); - String url = String.format(deepUrl + "/token/has?userId=%s&masterToken=%s", userId, this.masterToken); + String url = String.format("/token/has?userId=%s&masterToken=%s", userId, ""); RestTemplate restTemplate = new RestTemplate(); @@ -85,7 +85,7 @@ public boolean hasUser() { String getAdminToken() { String userId = this.getUserId(); - String url = String.format(deepUrl + "/token?userId=%s&masterToken=%s", userId, this.masterToken); + String url = String.format("/token?userId=%s&masterToken=%s", userId, ""); RestTemplate restTemplate = new RestTemplate(); diff --git a/GPTutor-Backend/src/main/resources/application.properties b/GPTutor-Backend/src/main/resources/application.properties index 8d8f58b9..543fc8e9 100644 --- a/GPTutor-Backend/src/main/resources/application.properties +++ b/GPTutor-Backend/src/main/resources/application.properties @@ -1,8 +1,6 @@ spring.datasource.url=jdbc:postgresql://${POSTGRES_HOST}:5432/postgres models.url=${MODELS_URL} rag.url=${RAG_URL} -master-token=${MASTER_TOKEN} -deep-url=${DEEP_URL} tg-token=${TG_TOKEN} spring.datasource.username=${POSTGRES_USER} spring.datasource.password=${POSTGRES_PASSWORD} diff --git a/GPTutor-Frontend-v2/.dockerignore b/GPTutor-Frontend-v2/.dockerignore new file mode 100644 index 00000000..77910e12 --- /dev/null +++ b/GPTutor-Frontend-v2/.dockerignore @@ -0,0 +1,17 @@ +node_modules +npm-debug.log +.git +.gitignore +README.md +.env +.env.local +.env.development +.env.production +dist +build +coverage +.vscode +.idea +*.log +.DS_Store + diff --git a/GPTutor-Frontend-v2/.eslintrc.cjs b/GPTutor-Frontend-v2/.eslintrc.cjs new file mode 100644 index 00000000..b1e978da --- /dev/null +++ b/GPTutor-Frontend-v2/.eslintrc.cjs @@ -0,0 +1,15 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + }, +}; diff --git a/GPTutor-Frontend-v2/.nginx/nginx.conf b/GPTutor-Frontend-v2/.nginx/nginx.conf new file mode 100644 index 00000000..c282032f --- /dev/null +++ b/GPTutor-Frontend-v2/.nginx/nginx.conf @@ -0,0 +1,16 @@ +worker_processes 4; + +events { worker_connections 1024; } + +http { + server { + listen 80; + + root /usr/share/nginx/html; + include /etc/nginx/mime.types; + + location /appui { + try_files $uri /index.html; + } + } +} \ No newline at end of file diff --git a/GPTutor-Frontend-v2/Dockerfile b/GPTutor-Frontend-v2/Dockerfile new file mode 100644 index 00000000..8ff8f12b --- /dev/null +++ b/GPTutor-Frontend-v2/Dockerfile @@ -0,0 +1,85 @@ +# Build stage +FROM node:20-alpine AS builder + +WORKDIR /app + +COPY package*.json ./ + +RUN npm ci + +COPY . . + +ARG VITE_API_URL +ARG VITE_VK_APP_ID + +ENV VITE_API_URL=$VITE_API_URL +ENV VITE_VK_APP_ID=$VITE_VK_APP_ID + +# Debug: Show environment variables +RUN echo "VITE_API_URL: $VITE_API_URL" +RUN echo "VITE_VK_APP_ID: $VITE_VK_APP_ID" + +RUN npm run build + +# Debug: List build output +RUN ls -la /app/build/ + +# Production stage +FROM nginx:alpine + +# Install wget for health check +RUN apk add --no-cache wget + +# Remove default nginx config +RUN rm /etc/nginx/conf.d/default.conf + +# Create custom nginx config for SPA +RUN echo 'server { \ + listen 80; \ + server_name localhost; \ + root /usr/share/nginx/html; \ + index index.html; \ + \ + # Gzip compression \ + gzip on; \ + gzip_vary on; \ + gzip_min_length 1024; \ + gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; \ + \ + # Security headers for VK Mini Apps (allow iframe embedding) \ + add_header X-Content-Type-Options "nosniff" always; \ + add_header X-XSS-Protection "1; mode=block" always; \ + # Allow embedding in VK iframe \ + add_header Content-Security-Policy "frame-ancestors 'self' https://vk.com https://*.vk.com https://vk.ru https://*.vk.ru" always; \ + \ + # Cache static assets \ + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { \ + expires 1y; \ + add_header Cache-Control "public, immutable"; \ + } \ + \ + # SPA routing - all routes go to index.html \ + location / { \ + try_files $uri $uri/ /index.html; \ + } \ + \ + # Health check endpoint \ + location /health { \ + access_log off; \ + return 200 "healthy\n"; \ + add_header Content-Type text/plain; \ + } \ +}' > /etc/nginx/conf.d/default.conf + +# Copy built files from builder +COPY --from=builder /app/build /usr/share/nginx/html + +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --quiet --tries=1 --spider http://localhost/health || exit 1 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] + diff --git a/GPTutor-Frontend-v2/GENERATED_IMAGES_FEATURE.md b/GPTutor-Frontend-v2/GENERATED_IMAGES_FEATURE.md new file mode 100644 index 00000000..2f86e787 --- /dev/null +++ b/GPTutor-Frontend-v2/GENERATED_IMAGES_FEATURE.md @@ -0,0 +1,280 @@ +# 🎨 Поддержка сгенерированных изображений + +## ✨ Что добавлено + +Добавлена поддержка изображений, которые генерирует AI модель (например, `google/gemini-2.5-flash-image`). + +## 🔧 Технические детали + +### 1. MessageModel + +**Добавлено новое поле:** +```typescript +generatedImages: ImageAttachment[] = []; // Изображения от модели +``` + +**Новые методы:** +- `addGeneratedImages(images)` - добавляет изображения от модели +- `setGeneratedImages(images)` - устанавливает изображения +- `hasGeneratedImages` - getter для проверки наличия изображений + +### 2. ChatViewModel + +**Обновлен метод `streamCompletion`:** +- Парсит `delta.images` из ответа API +- Добавляет сгенерированные изображения в сообщение +- Логирует процесс для отладки + +### 3. MessageItem + +**Добавлен блок отображения:** +- Изображения отображаются после контента +- Responsive дизайн (max-width: 512px) +- Lazy loading для оптимизации +- Скругленные углы (border-radius: 8px) + +--- + +## 📊 Формат API + +### Запрос к модели с генерацией изображений: + +```typescript +{ + "model": "google/gemini-2.5-flash-image", + "messages": [ + { + "role": "user", + "content": "Нарисуй кота" + } + ], + "stream": true +} +``` + +### Ответ от модели (SSE): + +```json +{ + "id": "gen-1760008258-...", + "provider": "Google AI Studio", + "model": "google/gemini-2.5-flash-image", + "object": "chat.completion.chunk", + "created": 1760008258, + "choices": [ + { + "index": 0, + "delta": { + "role": "assistant", + "content": "", + "images": [ + { + "type": "image_url", + "image_url": { + "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA..." + }, + "index": 0 + } + ] + }, + "finish_reason": "stop" + } + ] +} +``` + +--- + +## 🎨 Отображение на UI + +### Структура сообщения: + +``` +┌─────────────────────────────────────┐ +│ 🤖 Assistant │ +│ │ +│ Вот изображение кота: │ ← content +│ │ +│ ┌─────────────────────────────────┐ │ +│ │ │ │ +│ │ [Generated Image] │ │ ← generatedImages +│ │ │ │ +│ └─────────────────────────────────┘ │ +│ │ +│ 💰 Cost: 0.05 RUB │ +└─────────────────────────────────────┘ +``` + +### CSS стили: + +```css +.generated-images { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 14px; +} + +.generated-image { + width: 100%; + max-width: 512px; + height: auto; + border-radius: 8px; +} +``` + +--- + +## 🔄 Процесс обработки + +``` +1. Модель генерирует изображение + ↓ +2. Backend отправляет SSE с delta.images + ↓ +3. ChatViewModel парсит images из delta + ↓ +4. message.addGeneratedImages(images) + ↓ +5. MessageModel сохраняет в generatedImages[] + ↓ +6. MessageItem отображает изображения + ↓ +7. Пользователь видит результат +``` + +--- + +## 🎯 Поддерживаемые форматы изображений + +### Base64: +```typescript +{ + "type": "image_url", + "image_url": { + "url": "data:image/png;base64,iVBORw0KGgo..." + } +} +``` + +### URL: +```typescript +{ + "type": "image_url", + "image_url": { + "url": "https://example.com/image.png" + } +} +``` + +--- + +## 💡 Примеры использования + +### Генерация изображения: + +**Пользователь:** +> Нарисуй красивый закат над морем + +**Ассистент:** +> Вот изображение красивого заката над морем: +> +> [Изображение отображается] + +### Множественные изображения: + +Если модель вернет несколько изображений, они отобразятся вертикально: + +```typescript +delta.images = [ + { type: "image_url", image_url: { url: "data:image/png..." } }, + { type: "image_url", image_url: { url: "data:image/png..." } }, + { type: "image_url", image_url: { url: "data:image/png..." } } +] +``` + +--- + +## 🐛 Отладка + +### Проверка в консоли: + +```javascript +// В ChatViewModel при получении изображений: +console.log("Parsed images:", images); +// [{ type: "image_url", image_url: { url: "data:..." } }] + +console.log("Adding generated images:", images.length, "images"); +// Adding generated images: 1 images + +// В MessageModel: +message.generatedImages +// [{ type: "image_url", ... }] +``` + +### Проверка в DevTools: + +```javascript +// Посмотреть сгенерированные изображения +chatViewModel.messages.forEach(msg => { + if (msg.generatedImages.length > 0) { + console.log('Message with images:', msg.id, msg.generatedImages); + } +}); +``` + +--- + +## 📱 Responsive дизайн + +Изображения адаптируются под размер экрана: + +- **Desktop:** max-width: 512px +- **Mobile:** width: 100% (растягивается на всю ширину) +- **Aspect ratio:** сохраняется автоматически (height: auto) + +--- + +## 🚀 Модели с поддержкой генерации изображений + +- `google/gemini-2.5-flash-image` ✅ +- `google/gemini-pro-vision-image` (если есть) +- Другие модели с суффиксом `-image` + +--- + +## ✅ Что работает + +- ✅ Парсинг изображений из SSE +- ✅ Сохранение в сообщении +- ✅ Отображение на UI +- ✅ Поддержка Base64 +- ✅ Поддержка URL +- ✅ Множественные изображения +- ✅ Lazy loading +- ✅ Responsive дизайн + +--- + +## 🔮 Будущие улучшения + +- [ ] Кнопка "Скачать изображение" +- [ ] Полноэкранный просмотр +- [ ] Галерея для множественных изображений +- [ ] Копирование изображения в буфер обмена +- [ ] Поделиться изображением + +--- + +## 📚 Связанные файлы + +- `MessageModel.ts` - модель сообщения с изображениями +- `ChatViewModel.ts` - обработка SSE и парсинг images +- `MessageItem.tsx` - отображение изображений на UI + +--- + +## 🎉 Готово! + +Теперь модель может возвращать изображения, и они будут автоматически отображаться в чате! 🎨 + diff --git a/GPTutor-Frontend-v2/index.html b/GPTutor-Frontend-v2/index.html new file mode 100644 index 00000000..63011420 --- /dev/null +++ b/GPTutor-Frontend-v2/index.html @@ -0,0 +1,16 @@ + + + + + + + VK Mini App Boilerplate + + +
+ + + diff --git a/GPTutor-Frontend-v2/package-lock.json b/GPTutor-Frontend-v2/package-lock.json new file mode 100644 index 00000000..7591d7f7 --- /dev/null +++ b/GPTutor-Frontend-v2/package-lock.json @@ -0,0 +1,7589 @@ +{ + "name": "gptutor-frontend-v2", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gptutor-frontend-v2", + "version": "0.0.0", + "dependencies": { + "@vkontakte/icons": "^3.1.1", + "@vkontakte/vk-bridge": "^2.13.0", + "@vkontakte/vk-bridge-react": "^1.0.1", + "@vkontakte/vk-mini-apps-router": "^1.7.6", + "@vkontakte/vkui": "^7.0.0", + "markdown-it": "^14.1.0", + "markdown-it-link-attributes": "^4.0.1", + "mobx": "^6.15.0", + "mobx-react-lite": "^4.1.1", + "prismjs": "^1.30.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/markdown-it": "^14.1.2", + "@types/prismjs": "^1.26.5", + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@typescript-eslint/eslint-plugin": "^6.14.0", + "@typescript-eslint/parser": "^6.14.0", + "@vitejs/plugin-legacy": "^5.3.1", + "@vitejs/plugin-react": "^4.2.1", + "@vkontakte/vk-miniapps-deploy": "^0.1.6", + "@vkontakte/vk-tunnel": "^0.2.4", + "eruda": "^3.0.1", + "esbuild": "~0.20.0", + "eslint": "^8.55.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "terser": "^5.4.0", + "typescript": "^5.2.2", + "vite": "^5.0.8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@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-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", + "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", + "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "regexpu-core": "^6.2.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", + "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "debug": "^4.4.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.10" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", + "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", + "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", + "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", + "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", + "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.4.tgz", + "integrity": "sha512-1yxmvN0MJHOhPVmAsmoW5liWwoILobu/d/ShymZmj867bAdxGbehIrew1DuLpw2Ukv+qDSSPQdYW1dLNE7t11A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", + "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", + "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz", + "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", + "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", + "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", + "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", + "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", + "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", + "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", + "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", + "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.3.tgz", + "integrity": "sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.27.1", + "@babel/plugin-syntax-import-attributes": "^7.27.1", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.28.0", + "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.0", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-class-static-block": "^7.28.3", + "@babel/plugin-transform-classes": "^7.28.3", + "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", + "@babel/plugin-transform-exponentiation-operator": "^7.27.1", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.27.1", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", + "@babel/plugin-transform-numeric-separator": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.28.0", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.27.1", + "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.28.3", + "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "core-js-compat": "^3.43.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz", + "integrity": "sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.3.tgz", + "integrity": "sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.3.tgz", + "integrity": "sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.3.tgz", + "integrity": "sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.3.tgz", + "integrity": "sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.3.tgz", + "integrity": "sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.3.tgz", + "integrity": "sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.3.tgz", + "integrity": "sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.3.tgz", + "integrity": "sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.3.tgz", + "integrity": "sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.3.tgz", + "integrity": "sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.3.tgz", + "integrity": "sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.3.tgz", + "integrity": "sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.3.tgz", + "integrity": "sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.3.tgz", + "integrity": "sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.3.tgz", + "integrity": "sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.3.tgz", + "integrity": "sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.3.tgz", + "integrity": "sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.3.tgz", + "integrity": "sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.3.tgz", + "integrity": "sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.3.tgz", + "integrity": "sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.3.tgz", + "integrity": "sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "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/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prismjs": { + "version": "1.26.5", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", + "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.24", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz", + "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-legacy": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-legacy/-/plugin-legacy-5.4.3.tgz", + "integrity": "sha512-wsyXK9mascyplcqvww1gA1xYiy29iRHfyciw+a0t7qRNdzX6PdfSWmOoCi74epr87DujM+5J+rnnSv+4PazqVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.8", + "@babel/preset-env": "^7.25.8", + "browserslist": "^4.24.0", + "browserslist-to-esbuild": "^2.1.1", + "core-js": "^3.38.1", + "magic-string": "^0.30.12", + "regenerator-runtime": "^0.14.1", + "systemjs": "^6.15.1" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "peerDependencies": { + "terser": "^5.4.0", + "vite": "^5.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vkontakte/icons": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@vkontakte/icons/-/icons-3.19.0.tgz", + "integrity": "sha512-XuEVblOpbFamvRzUXDrKFNnt6QOeNJfqn/oTE97ZonFb+AIew+cB/6h7S0Y4agyvDvHqxI2WdJmtmTe7w1BsEw==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.17", + "@vkontakte/icons-sprite": "^3.1.1" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@vkontakte/icons-sprite": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vkontakte/icons-sprite/-/icons-sprite-3.1.1.tgz", + "integrity": "sha512-qFrTDmijeZpmcP779DRBGfBr9Wn5sdPrP39ZL/4fG5pg/D6+Fc6sYuC5N088vOVrGP6oro07B45liJrfhv5LfA==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.17" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@vkontakte/vk-bridge": { + "version": "2.15.9", + "resolved": "https://registry.npmjs.org/@vkontakte/vk-bridge/-/vk-bridge-2.15.9.tgz", + "integrity": "sha512-YonvPu/ozRXm/iE1vHfZLKJwCzaAol/ZnQ/zfFQFnRlkQh5uWQOuif3GdSFUrVYYNvlMY752Zl0lWVStnhSKVQ==", + "license": "MIT" + }, + "node_modules/@vkontakte/vk-bridge-react": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vkontakte/vk-bridge-react/-/vk-bridge-react-1.1.0.tgz", + "integrity": "sha512-yHu/1q7/p/Wm3Wumj8C+h71VliCIMLB+6AJYUNsKg9PksL76Dl1rw2gfcgNJobd4idFDRL4J4kAMOO6jQIHIGQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "@vkontakte/vk-bridge": "^2.15.1", + "react": "^17.0.0 || ^18.1.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@vkontakte/vk-mini-apps-router": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@vkontakte/vk-mini-apps-router/-/vk-mini-apps-router-1.8.1.tgz", + "integrity": "sha512-Fb1oNmPDOYrRQ8ySG9++ay7E+rgH5TAZV8MeSTqPjeQBpipVxUfRmk4py4IW8omke7gn5sjDJ2fFOXPyUWmUcg==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "^1.13.0" + }, + "peerDependencies": { + "@vkontakte/vk-bridge": "^2.7.2", + "@vkontakte/vkui": "^5.1.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vkontakte/vk-miniapps-deploy": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@vkontakte/vk-miniapps-deploy/-/vk-miniapps-deploy-0.1.9.tgz", + "integrity": "sha512-aX9APiMA23u1gVzxUbpuachS6VMMap4WyYyq6ZUxsXFsUNUa0fOgxS092nuM3YDZaw3aml20MRbsh/Gj0NpB+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^3.2.0", + "chalk": "^3.0.0", + "configstore": "^5.0.0", + "form-data": "^3.0.0", + "fs-extra": "^8.0.1", + "node-fetch": "^2.6.0", + "prompts": "^2.1.0", + "require-module": "^0.1.0", + "zip-a-folder": "0.0.12" + }, + "bin": { + "vk-miniapps-deploy": "bin/vk-miniapps-deploy" + }, + "engines": { + "node": ">=8.10" + } + }, + "node_modules/@vkontakte/vk-tunnel": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@vkontakte/vk-tunnel/-/vk-tunnel-0.2.9.tgz", + "integrity": "sha512-5tMvYkkQEfqRlmaheBoHxbwHZ+R0UJo3eBr4zuCP30yi0HF9AgJf3NwUWWdtzqMpFfW/b55lgSh16Q1j5shSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.8.4", + "axios-retry": "^4.4.0", + "chalk": "^3.0.0", + "configstore": "^6.0.0", + "minimist": "^1.2.8", + "pino": "^8.19.0", + "pino-pretty": "^10.3.1", + "prompts": "^2.3.2", + "ws": "^8.16.0" + }, + "bin": { + "vk-tunnel": "bin/vk-tunnel.js" + } + }, + "node_modules/@vkontakte/vk-tunnel/node_modules/configstore": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-6.0.0.tgz", + "integrity": "sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dot-prop": "^6.0.1", + "graceful-fs": "^4.2.6", + "unique-string": "^3.0.0", + "write-file-atomic": "^3.0.3", + "xdg-basedir": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/yeoman/configstore?sponsor=1" + } + }, + "node_modules/@vkontakte/vk-tunnel/node_modules/crypto-random-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", + "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vkontakte/vk-tunnel/node_modules/dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vkontakte/vk-tunnel/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vkontakte/vk-tunnel/node_modules/unique-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", + "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "crypto-random-string": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vkontakte/vk-tunnel/node_modules/xdg-basedir": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", + "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vkontakte/vkjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@vkontakte/vkjs/-/vkjs-2.0.1.tgz", + "integrity": "sha512-VPce+45X7mXpukkLwDaXwFfqfq0NZecoZ3Js3XwXFfhwNanetxMo/6XqG3sV319mxJoAqW8gMQoSv+ExLTlmYg==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.0", + "clsx": "^2.1.1" + } + }, + "node_modules/@vkontakte/vkui": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@vkontakte/vkui/-/vkui-7.7.1.tgz", + "integrity": "sha512-GQbSBfpYEHC6V70gPHSVjdY7e7/XM8MeFHFNPYpZTuGTijbY0sC7H06D/NZ/zyxRzPAli7aGuVDOKSvIajh6bQ==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.17", + "@vkontakte/icons": "^3.0.0", + "@vkontakte/vkjs": "^2.0.1", + "@vkontakte/vkui-date-fns-tz": "^0.0.6", + "@vkontakte/vkui-floating-ui": "^0.2.7" + }, + "peerDependencies": { + "react": "^18.2.0 || ^19.0.0", + "react-dom": "^18.2.0 || ^19.0.0" + } + }, + "node_modules/@vkontakte/vkui-date-fns-tz": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@vkontakte/vkui-date-fns-tz/-/vkui-date-fns-tz-0.0.6.tgz", + "integrity": "sha512-bC4yscqq+XnSsqJJLOBh1gMePVmS5FttsZv57o/rt1FbJpuDoC666zLnDat2nzCskU7j+oWi8oipfMdh1VLp8w==", + "license": "MIT" + }, + "node_modules/@vkontakte/vkui-floating-ui": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@vkontakte/vkui-floating-ui/-/vkui-floating-ui-0.2.7.tgz", + "integrity": "sha512-4cHwX2pJI4HBZM+Cp/E4f9tDIxU6AimvDEX5WEHGOLUkbCZDiSB2wavsZz128czedpmA/b+AX/l2codwKOhJtA==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.6", + "@swc/helpers": "^0.5.17" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/archiver": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-3.1.1.tgz", + "integrity": "sha512-5Hxxcig7gw5Jod/8Gq0OneVgLYET+oNHcxgWItq4TbhOzRLKNAFUb9edAftiMKXvXfCB0vbGrJdZDNq0dWMsxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^2.6.3", + "buffer-crc32": "^0.2.1", + "glob": "^7.1.4", + "readable-stream": "^3.4.0", + "tar-stream": "^2.1.0", + "zip-stream": "^2.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/archiver/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios-retry": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", + "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "is-retry-allowed": "^2.2.0" + }, + "peerDependencies": { + "axios": "0.x || 1.x" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", + "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.7", + "@babel/helper-define-polyfill-provider": "^0.6.5", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", + "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.8.tgz", + "integrity": "sha512-be0PUaPsQX/gPWWgFsdD+GFzaoig5PXaUC1xLkQiYdDnANU8sMnHoQd8JhbJQuvTWrWLyeFN9Imb5Qtfvr4RrQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.26.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", + "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", + "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.8.3", + "caniuse-lite": "^1.0.30001741", + "electron-to-chromium": "^1.5.218", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/browserslist-to-esbuild": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/browserslist-to-esbuild/-/browserslist-to-esbuild-2.1.1.tgz", + "integrity": "sha512-KN+mty6C3e9AN8Z5dI1xeN15ExcRNeISoC3g7V0Kax/MMF9MSoYA2G7lkTTcVUFntiEjkpI0HNgqJC1NjdyNUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "meow": "^13.0.0" + }, + "bin": { + "browserslist-to-esbuild": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "browserslist": "*" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001745", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz", + "integrity": "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/compress-commons": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-2.1.1.tgz", + "integrity": "sha512-eVw6n7CnEMFzc3duyFVrQEuY1BlHR3rYsSztyG32ibGMW722i3C6IizEGMFmfMU+A+fALvBIwxN3czffTcdA+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^3.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^2.3.6" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/compress-commons/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/compress-commons/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js": { + "version": "3.45.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz", + "integrity": "sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.45.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.1.tgz", + "integrity": "sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/crc/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/crc32-stream": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-3.0.1.tgz", + "integrity": "sha512-mctvpXlbzsvK+6z8kJwSJ5crm7yBwrQMTybJzMw1O4lLGJqjlDCXY2Zw7KheiA6XBEcBmfLx1D88mjRGVJtY9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc": "^3.4.4", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 6.9.0" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "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/electron-to-chromium": { + "version": "1.5.227", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.227.tgz", + "integrity": "sha512-ITxuoPfJu3lsNWUi2lBM2PaBPYgH3uqmxut5vmBxgYvyI4AlJ6P3Cai1O76mOrkJCBzq0IxWg/NtqOrpu/0gKA==", + "dev": true, + "license": "ISC" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/eruda": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eruda/-/eruda-3.4.3.tgz", + "integrity": "sha512-J2TsF4dXSspOXev5bJ6mljv0dRrxj21wklrDzbvPmYaEmVoC+2psylyRi70nUPFh1mTQfIBsSusUtAMZtUN+/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.22", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.22.tgz", + "integrity": "sha512-atkAG6QaJMGoTLc4MDAP+rqZcfwQuTIh2IqHWFLy2TEjxr0MOK+5BSG4RzL2564AAPpZkDRsZXAUz68kjnU6Ug==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-copy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz", + "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "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/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-retry-allowed": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", + "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-link-attributes": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/markdown-it-link-attributes/-/markdown-it-link-attributes-4.0.1.tgz", + "integrity": "sha512-pg5OK0jPLg62H4k7M9mRJLT61gUp9nvG0XveKYHMOOluASo9OEF13WlXrpAp2aj35LbedAy3QOCgQCw0tkLKAQ==", + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, + "node_modules/meow": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mobx": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.15.0.tgz", + "integrity": "sha512-UczzB+0nnwGotYSgllfARAqWCJ5e/skuV2K/l+Zyck/H6pJIhLXuBnz+6vn2i211o7DtbE78HQtsYEKICHGI+g==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + } + }, + "node_modules/mobx-react-lite": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-4.1.1.tgz", + "integrity": "sha512-iUxiMpsvNraCKXU+yPotsOncNNmyeS2B5DKL+TL6Tar/xm+wwNJAubJmtRSeAoYawdZqwv8Z/+5nPRHeQxTiXg==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + }, + "peerDependencies": { + "mobx": "^6.9.0", + "react": "^16.8.0 || ^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-releases": { + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", + "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.21.0.tgz", + "integrity": "sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.2.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^3.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.6.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", + "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-10.3.1.tgz", + "integrity": "sha512-az8JbIYeN/1iLj2t0jR9DV48/LQ3RC6hZPpapKPkb84Q+yTidMCpgWxIT3N0flnBDilyBQ1luWNpOeJptjdp/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.0.0", + "pump": "^3.0.0", + "readable-stream": "^4.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-std-serializers": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "dev": true, + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true, + "license": "MIT" + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/require-module": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/require-module/-/require-module-0.1.0.tgz", + "integrity": "sha512-fbr7gXnwot8k98dOUIq9KA4tvEot+CNMg1GR6j1v+7gI3aECMeyxmw2Ux0RWecPR6GfLqktVJ84GlTXoFlS2Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "~0.6.1" + } + }, + "node_modules/require-module/node_modules/resolve": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.6.3.tgz", + "integrity": "sha512-UHBY3viPlJKf85YijDUcikKX6tmF4SokIDp518ZDVT92JNDcG5uKIthaT/owt3Sar0lwtOafsQuwrg22/v2Dwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.3.tgz", + "integrity": "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.3", + "@rollup/rollup-android-arm64": "4.52.3", + "@rollup/rollup-darwin-arm64": "4.52.3", + "@rollup/rollup-darwin-x64": "4.52.3", + "@rollup/rollup-freebsd-arm64": "4.52.3", + "@rollup/rollup-freebsd-x64": "4.52.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.3", + "@rollup/rollup-linux-arm-musleabihf": "4.52.3", + "@rollup/rollup-linux-arm64-gnu": "4.52.3", + "@rollup/rollup-linux-arm64-musl": "4.52.3", + "@rollup/rollup-linux-loong64-gnu": "4.52.3", + "@rollup/rollup-linux-ppc64-gnu": "4.52.3", + "@rollup/rollup-linux-riscv64-gnu": "4.52.3", + "@rollup/rollup-linux-riscv64-musl": "4.52.3", + "@rollup/rollup-linux-s390x-gnu": "4.52.3", + "@rollup/rollup-linux-x64-gnu": "4.52.3", + "@rollup/rollup-linux-x64-musl": "4.52.3", + "@rollup/rollup-openharmony-arm64": "4.52.3", + "@rollup/rollup-win32-arm64-msvc": "4.52.3", + "@rollup/rollup-win32-ia32-msvc": "4.52.3", + "@rollup/rollup-win32-x64-gnu": "4.52.3", + "@rollup/rollup-win32-x64-msvc": "4.52.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/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==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sonic-boom": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz", + "integrity": "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/systemjs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/systemjs/-/systemjs-6.15.1.tgz", + "integrity": "sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/terser": { + "version": "5.44.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", + "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/thread-stream": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz", + "integrity": "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.20", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz", + "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-a-folder": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/zip-a-folder/-/zip-a-folder-0.0.12.tgz", + "integrity": "sha512-wZGiWgp3z2TocBlzx3S5tsLgPbT39qG2uIZmn2MhYLVjhKIr2nMhg7i4iPDL4W3XvMDaOEEVU5ZB0Y/Pt6BLvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver": "^3.1.1" + } + }, + "node_modules/zip-stream": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.1.3.tgz", + "integrity": "sha512-EkXc2JGcKhO5N5aZ7TmuNo45budRaFGHOmz24wtJR7znbNqDPmdZtUauKX6et8KAVseAMBOyWJqEpXcHTBsh7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "compress-commons": "^2.1.1", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + } + } +} diff --git a/GPTutor-Frontend-v2/package.json b/GPTutor-Frontend-v2/package.json new file mode 100644 index 00000000..30ac59c0 --- /dev/null +++ b/GPTutor-Frontend-v2/package.json @@ -0,0 +1,48 @@ +{ + "name": "gptutor-frontend-v2", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "start": "vite", + "build": "tsc && vite build", + "lint": "eslint --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview", + "tunnel": "vk-tunnel --http-protocol=http --host=localhost --port=5173", + "deploy": "vite build && vk-miniapps-deploy" + }, + "dependencies": { + "@vkontakte/icons": "^3.1.1", + "@vkontakte/vk-bridge": "^2.13.0", + "@vkontakte/vk-bridge-react": "^1.0.1", + "@vkontakte/vk-mini-apps-router": "^1.7.6", + "@vkontakte/vkui": "^7.0.0", + "markdown-it": "^14.1.0", + "markdown-it-link-attributes": "^4.0.1", + "mobx": "^6.15.0", + "mobx-react-lite": "^4.1.1", + "prismjs": "^1.30.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/markdown-it": "^14.1.2", + "@types/prismjs": "^1.26.5", + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@typescript-eslint/eslint-plugin": "^6.14.0", + "@typescript-eslint/parser": "^6.14.0", + "@vitejs/plugin-legacy": "^5.3.1", + "@vitejs/plugin-react": "^4.2.1", + "@vkontakte/vk-miniapps-deploy": "^0.1.6", + "@vkontakte/vk-tunnel": "^0.2.4", + "eruda": "^3.0.1", + "esbuild": "~0.20.0", + "eslint": "^8.55.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "terser": "^5.4.0", + "typescript": "^5.2.2", + "vite": "^5.0.8" + } +} diff --git a/GPTutor-Frontend-v2/public/logo.svg b/GPTutor-Frontend-v2/public/logo.svg new file mode 100644 index 00000000..40d5b0e4 --- /dev/null +++ b/GPTutor-Frontend-v2/public/logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/GPTutor-Frontend-v2/src/App.css b/GPTutor-Frontend-v2/src/App.css new file mode 100644 index 00000000..1c8c796d --- /dev/null +++ b/GPTutor-Frontend-v2/src/App.css @@ -0,0 +1,176 @@ +.vkuiPanelHeader__in { + background: var(--vkui--color_background_contrast_themed) +} + + +.code-block { + position: relative; + border-radius: 8px; + overflow: hidden; +} + +.code-block pre { + position: relative; + border-radius: 8px; + overflow: hidden; + border: 1px solid var(--vkui--color_separator_primary); + margin: 0; + padding: 16px; + background: transparent; + overflow-x: auto; +} + +.code-block code { + background: transparent; + padding: 0; + border-radius: 0; + font-size: inherit; + color: inherit; +} + +/* Code buttons */ +.code-buttons { + position: absolute; + top: 8px; + right: 8px; + display: flex; + gap: 8px; + opacity: 0; + transition: opacity 0.2s ease; +} + +[data-pre-container]:hover .code-buttons { + opacity: 1; +} + +.code-buttons button { + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + color: #d4d4d4; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + cursor: pointer; + transition: all 0.2s ease; +} + +.code-buttons button:hover { + background: rgba(255, 255, 255, 0.2); + border-color: rgba(255, 255, 255, 0.3); +} + +code { + font-size: 15px !important; +} + +.chat-container pre { + background-color: hsl(230, 1%, 98%); + max-width: calc(100vw - 132px); + overflow-x: auto; + margin-top: 8px; +} + +/* Markdown Tables */ +.code-block table { + border-collapse: collapse; + width: 100%; + margin: 16px 0; + background: var(--vkui--color_background); + border: 1px solid var(--vkui--color_separator_primary); + border-radius: 8px; + overflow: hidden; + font-size: 14px; +} + +.code-block thead { + background: var(--vkui--color_background_secondary); +} + +.code-block th { + padding: 12px 16px; + text-align: left; + font-weight: 600; + color: var(--vkui--color_text_primary); + border-bottom: 2px solid var(--vkui--color_separator_primary); + border-right: 1px solid var(--vkui--color_separator_primary); +} + +.code-block th:last-child { + border-right: none; +} + +.code-block td { + padding: 10px 16px; + border-bottom: 1px solid var(--vkui--color_separator_primary); + border-right: 1px solid var(--vkui--color_separator_primary); + color: var(--vkui--color_text_primary); +} + +.code-block td:last-child { + border-right: none; +} + +.code-block tr:last-child td { + border-bottom: none; +} + +.code-block tbody tr:hover { + background: var(--vkui--color_background_hover); +} + +/* Table wrapper for horizontal scroll */ +.code-block .table-wrapper { + overflow-x: auto; + margin: 16px 0; +} + +.code-block .table-wrapper table { + margin: 0; +} + +.code-copy-button { + transition: all 0.2s ease; +} + +.code-copy-button:hover { + background-color: var(--vkui--color_background_tertiary) !important; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + + +.forceCardStyle .vkuiCard__host { + width: 100% !important; + max-width: none !important; + min-width: 0 !important; + margin-left: 0 !important; + margin-right: 0 !important; +} + +.forceCardStyle .vkuiCard__modeOutline { + padding-left: 0 !important; + padding-right: 0 !important; +} + +/* Inline Citation Links [N] в тексте */ +.inline-citation { + color: var(--vkui--color_text_link) !important; + text-decoration: none !important; + font-weight: 500; + transition: opacity 0.2s ease; + cursor: pointer; +} + +.inline-citation:hover { + opacity: 0.7; + text-decoration: underline !important; +} + +.inline-citation:active { + opacity: 0.5; +} + +ol, ul { + margin-left: 4px; + padding-left: 12px; +} diff --git a/GPTutor-Frontend-v2/src/App.tsx b/GPTutor-Frontend-v2/src/App.tsx new file mode 100644 index 00000000..f95cd8e9 --- /dev/null +++ b/GPTutor-Frontend-v2/src/App.tsx @@ -0,0 +1,112 @@ +import { useEffect } from "react"; +import { + Epic, + ModalRoot, + SplitCol, + SplitLayout, + Tabbar, + TabbarItem, + View, +} from "@vkontakte/vkui"; +import { + useActiveVkuiLocation, + useRouteNavigator, +} from "@vkontakte/vk-mini-apps-router"; +import { useAppearance } from "@vkontakte/vk-bridge-react"; +import { OneDark } from "./themes/OneDark"; +import { OneLight } from "./themes/OneLight"; +import { ThemeProvider } from "./contexts/ThemeContext"; +import { SnackbarProvider } from "./hooks/useSnackbar"; + +import { Chat, Home, Models, Persik, Profile } from "./panels"; +import { DEFAULT_VIEW_PANELS, MODALS } from "./routes.ts"; +import { + Icon28HomeOutline, + Icon28MessageOutline, + Icon28UserCircleOutline, +} from "@vkontakte/icons"; + +import "./App.css"; +import { userViewModel } from "./viewModels/UserViewModel.ts"; +import { TopUpBalanceModal } from "./modals"; + +export const App = () => { + const { view: activeView, panel: activePanel = DEFAULT_VIEW_PANELS.HOME, modal: activeModal } = + useActiveVkuiLocation(); + const routeNavigator = useRouteNavigator(); + + const vkAppearance = useAppearance(); + const isDarkTheme = vkAppearance === "dark"; + + useEffect(() => { + userViewModel.getUser(); + }, []); + + const modal = ( + + + + ); + + return ( + + + + + + routeNavigator.push("/")} + label="Главная" + > + + + + routeNavigator.push(`/${DEFAULT_VIEW_PANELS.PROFILE}`) + } + label="Профиль" + > + + + + routeNavigator.push(`/${DEFAULT_VIEW_PANELS.CHAT}`) + } + label="Чат" + > + + + + ) : null + } + > + + + + + + + + + + + + + {modal} + + {/*{userViewModel.loading ? : null}*/} + + {/* Code Themes */} + {isDarkTheme ? : } + + + + ); +}; diff --git a/GPTutor-Frontend-v2/src/AppConfig.tsx b/GPTutor-Frontend-v2/src/AppConfig.tsx new file mode 100644 index 00000000..f999f392 --- /dev/null +++ b/GPTutor-Frontend-v2/src/AppConfig.tsx @@ -0,0 +1,41 @@ +import vkBridge, { + parseURLSearchParamsForGetLaunchParams, +} from "@vkontakte/vk-bridge"; +import { + useAdaptivity, + useAppearance, + useInsets, +} from "@vkontakte/vk-bridge-react"; +import { AdaptivityProvider, ConfigProvider, AppRoot } from "@vkontakte/vkui"; +import { RouterProvider } from "@vkontakte/vk-mini-apps-router"; +import "@vkontakte/vkui/dist/vkui.css"; + +import { transformVKBridgeAdaptivity } from "./utils"; +import { router } from "./routes.ts"; +import { App } from "./App.tsx"; + +export const AppConfig = () => { + const vkBridgeAppearance = useAppearance() || undefined; + const vkBridgeInsets = useInsets() || undefined; + const adaptivity = transformVKBridgeAdaptivity(useAdaptivity()); + const { vk_platform } = parseURLSearchParamsForGetLaunchParams( + window.location.search + ); + + return ( + + + + + + + + + + ); +}; diff --git a/GPTutor-Frontend-v2/src/api/config.ts b/GPTutor-Frontend-v2/src/api/config.ts new file mode 100644 index 00000000..b1969d8e --- /dev/null +++ b/GPTutor-Frontend-v2/src/api/config.ts @@ -0,0 +1,2 @@ +// API configuration +export const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001'; diff --git a/GPTutor-Frontend-v2/src/api/filesApi.ts b/GPTutor-Frontend-v2/src/api/filesApi.ts new file mode 100644 index 00000000..673ae721 --- /dev/null +++ b/GPTutor-Frontend-v2/src/api/filesApi.ts @@ -0,0 +1,103 @@ +import { API_BASE_URL } from './config'; + +export interface FileInfo { + id: string; + name: string; + type: string; + url: string; + size: number; + createdAt: string; +} + +export interface UploadFileResponse { + message: string; + file: FileInfo; + timestamp: string; +} + +export interface FilesListResponse { + success: boolean; + data: { + message: string; + files: FileInfo[]; + total: number; + timestamp: string; + }; +} + +export interface DeleteFileResponse { + success: boolean; + data: { + message: string; + fileId: string; + timestamp: string; + }; +} + +class FilesApi { + private baseUrl = API_BASE_URL; + + async uploadFile(file: File): Promise { + const formData = new FormData(); + formData.append('file', file); + + const response = await fetch(`${this.baseUrl}/upload`, { + method: 'POST', + body: formData, + headers: { + 'Authorization': `Bearer ${this.getAuthToken()}`, + }, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: 'Unknown error' })); + throw new Error(errorData.error || `HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + console.log('Upload response:', result); // Для отладки + + return result; + } + + async getFiles(): Promise { + const response = await fetch(`${this.baseUrl}/files`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${this.getAuthToken()}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: 'Unknown error' })); + throw new Error(errorData.error || `HTTP error! status: ${response.status}`); + } + + return response.json(); + } + + async deleteFile(fileId: string): Promise { + const response = await fetch(`${this.baseUrl}/files/${fileId}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${this.getAuthToken()}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: 'Unknown error' })); + throw new Error(errorData.error || `HTTP error! status: ${response.status}`); + } + + return response.json(); + } + + private getAuthToken(): string { + // Используем тот же подход, что и в profileApi + return window.location.toString(); + } +} + +export const filesApi = new FilesApi(); diff --git a/GPTutor-Frontend-v2/src/api/index.ts b/GPTutor-Frontend-v2/src/api/index.ts new file mode 100644 index 00000000..b15fe088 --- /dev/null +++ b/GPTutor-Frontend-v2/src/api/index.ts @@ -0,0 +1,7 @@ +// API exports +export * from './config'; +export * from './profileApi'; +export * from './modelsApi'; +export * from './modelsUtils'; +export * from './filesApi'; +export * from './paymentApi'; diff --git a/GPTutor-Frontend-v2/src/api/modelsApi.ts b/GPTutor-Frontend-v2/src/api/modelsApi.ts new file mode 100644 index 00000000..9a927655 --- /dev/null +++ b/GPTutor-Frontend-v2/src/api/modelsApi.ts @@ -0,0 +1,79 @@ +import { API_BASE_URL } from "./config.js"; + +export interface ModelData { + id: string; + name: string; + description: string; + pricing: { + prompt: number; + completion: number; + request: number; + image: number; + web_search: number; + internal_reasoning: number; + }; + context_length: number; + architecture: { + modality: string; + input_modalities: string[]; + output_modalities: string[]; + tokenizer: string; + instruct_type: string | null; + }; + top_provider: { + context_length: number; + max_completion_tokens: number; + is_moderated: boolean; + }; + supported_parameters: string[]; +} + +export interface ProcessedModel { + id: string; + name: string; + provider: string; + description: string; + price: string; + contextLength: number; + inputModalities: string[]; + isPopular: boolean; +} + +export interface ModelsResponse { + success: boolean; + data: { + models: ModelData[]; + }; +} + +class ModelsApiService { + private async makeRequest( + endpoint: string, + options: RequestInit = {} + ): Promise { + const url = `${API_BASE_URL}${endpoint}`; + + const response = await fetch(url, { + headers: { + "Content-Type": "application/json", + ...options.headers, + }, + ...options, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error( + errorData.message || `HTTP error! status: ${response.status}` + ); + } + + return response.json(); + } + + async getModels(): Promise { + return this.makeRequest("/v1/models"); + } +} + +export const modelsApi = new ModelsApiService(); diff --git a/GPTutor-Frontend-v2/src/api/modelsUtils.ts b/GPTutor-Frontend-v2/src/api/modelsUtils.ts new file mode 100644 index 00000000..9be59918 --- /dev/null +++ b/GPTutor-Frontend-v2/src/api/modelsUtils.ts @@ -0,0 +1,81 @@ +import { ModelData, ProcessedModel } from "./modelsApi"; + +// Функция для форматирования цены в рублях за миллион токенов +export const formatPriceInRubles = ( + promptPrice: number, + completionPrice: number +): string => { + if (promptPrice === 0 && completionPrice === 0) { + return "Бесплатно"; + } + + // Просто умножаем на миллион + const promptPricePerMillion = promptPrice * 1000000; + const completionPricePerMillion = completionPrice * 1000000; + + return `${promptPricePerMillion.toFixed( + 2 + )}₽ / ${completionPricePerMillion.toFixed(2)}₽`; +}; + +export const formatContextLength = (contextLength: number): string => { + if (contextLength >= 1000000) { + return `${(contextLength / 1000000).toFixed(1)}M context`; + } else if (contextLength >= 1000) { + return `${(contextLength / 1000).toFixed(0)}K context`; + } else { + return `${contextLength} context`; + } +}; + +// Функция для форматирования модальностей +export const formatModalities = (modalities: string[]): string => { + const modalityMap: { [key: string]: string } = { + text: "Текст", + image: "Изображения", + audio: "Аудио", + video: "Видео", + file: "Файлы", + function_calling: "Функции", + json_mode: "JSON", + vision: "Зрение", + speech: "Речь", + multimodal: "Мультимодальный", + }; + + return modalities + .map((modality) => modalityMap[modality.toLowerCase()] || modality) + .join(", "); +}; + +// Функция для обработки данных модели +export const processModelData = (model: ModelData): ProcessedModel => { + // Извлекаем провайдера из имени (например, "Google: Gemini 2.5" -> "Google") + const provider = model.name.split(":")[0] || "Unknown"; + + // Форматируем цены в рублях за миллион токенов + const promptPrice = model.pricing?.prompt || 0; + const completionPrice = model.pricing?.completion || 0; + const priceText = formatPriceInRubles(promptPrice, completionPrice); + + // Определяем, популярная ли модель (на основе известных моделей) + const isPopular = + model.name.toLowerCase().includes("gpt-4") || + model.name.toLowerCase().includes("gpt-5") || + model.name.toLowerCase().includes("claude") || + model.name.toLowerCase().includes("gemini-2.5"); + + return { + id: model.id, + name: model.name, + provider, + description: + model.description?.length > 100 + ? model.description.substring(0, 100) + "..." + : model.description || "", + price: priceText, + contextLength: model.context_length || 0, + inputModalities: model.architecture?.input_modalities || [], + isPopular, + }; +}; diff --git a/GPTutor-Frontend-v2/src/api/paymentApi.ts b/GPTutor-Frontend-v2/src/api/paymentApi.ts new file mode 100644 index 00000000..8e2b83fb --- /dev/null +++ b/GPTutor-Frontend-v2/src/api/paymentApi.ts @@ -0,0 +1,71 @@ +import { API_BASE_URL } from "./config"; + +export interface CreatePaymentRequest { + amount: number; + description?: string; + returnUrl?: string; +} + +export interface Payment { + id: string; + yookassaId: string; + amount: number; + status: string; + confirmationUrl?: string; + createdAt: string; +} + +export interface CreatePaymentResponse { + message: string; + payment: Payment; +} + +export interface GetPaymentsResponse { + message: string; + payments: Payment[]; +} + +class PaymentApiService { + private async makeRequest( + endpoint: string, + options: RequestInit = {} + ): Promise { + const url = `${API_BASE_URL}${endpoint}`; + + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${window.location}`, + "Content-Type": "application/json", + ...options.headers, + }, + ...options, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error( + errorData.error || errorData.message || `HTTP error! status: ${response.status}` + ); + } + + return response.json(); + } + + async createPayment(data: CreatePaymentRequest): Promise { + return this.makeRequest("/payments/create", { + method: "POST", + body: JSON.stringify(data), + }); + } + + async getUserPayments(): Promise { + return this.makeRequest("/payments"); + } + + async getPaymentInfo(yookassaId: string): Promise { + return this.makeRequest(`/payments/${yookassaId}`); + } +} + +export const paymentApi = new PaymentApiService(); + diff --git a/GPTutor-Frontend-v2/src/api/profileApi.ts b/GPTutor-Frontend-v2/src/api/profileApi.ts new file mode 100644 index 00000000..16777471 --- /dev/null +++ b/GPTutor-Frontend-v2/src/api/profileApi.ts @@ -0,0 +1,70 @@ +import { API_BASE_URL } from "./config.js"; + +export interface UserProfile { + id: number; + vkId: number; + balance: number; + apiKey: string; + isActive: boolean; + createdAt: string; + updatedAt: string; +} + +export interface VkData { + id: number; + first_name: string; + last_name: string; + photo_200: string; +} + +export interface ProfileResponse { + message: string; + vkData: VkData; + user: UserProfile; + timestamp: string; +} + +export interface UpdateTokenResponse { + message: string; + newApiKey: string; + user: Omit; + timestamp: string; +} + +class ProfileApiService { + private async makeRequest( + endpoint: string, + options: RequestInit = {} + ): Promise { + const url = `${API_BASE_URL}${endpoint}`; + + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${window.location}`, + ...options.headers, + }, + ...options, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error( + errorData.message || `HTTP error! status: ${response.status}` + ); + } + + return response.json(); + } + + async getProfile(): Promise { + return this.makeRequest("/user"); + } + + async updateToken(): Promise { + return this.makeRequest("/update-token", { + method: "POST", + }); + } +} + +export const profileApi = new ProfileApiService(); diff --git a/GPTutor-Frontend-v2/src/assets/persik.png b/GPTutor-Frontend-v2/src/assets/persik.png new file mode 100644 index 00000000..426fcd70 Binary files /dev/null and b/GPTutor-Frontend-v2/src/assets/persik.png differ diff --git a/GPTutor-Frontend-v2/src/components/AnimatedStatsCard.tsx b/GPTutor-Frontend-v2/src/components/AnimatedStatsCard.tsx new file mode 100644 index 00000000..ec4f4c72 --- /dev/null +++ b/GPTutor-Frontend-v2/src/components/AnimatedStatsCard.tsx @@ -0,0 +1,75 @@ +import { FC, useState, useEffect } from 'react'; +import { Card, Title, Text } from '@vkontakte/vkui'; + +interface AnimatedStatsCardProps { + icon: React.ComponentType<{ style?: React.CSSProperties }>; + value: string; + label: string; + delay?: number; +} + +export const AnimatedStatsCard: FC = ({ + icon: Icon, + value, + label, + delay = 0 +}) => { + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + const timer = setTimeout(() => { + setIsVisible(true); + }, delay); + + return () => clearTimeout(timer); + }, [delay]); + + return ( + + + + {value} + + + {label} + + + ); +}; + + + + + + + + + + + diff --git a/GPTutor-Frontend-v2/src/components/CitationLink.tsx b/GPTutor-Frontend-v2/src/components/CitationLink.tsx new file mode 100644 index 00000000..390c2655 --- /dev/null +++ b/GPTutor-Frontend-v2/src/components/CitationLink.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import { Link } from "@vkontakte/vkui"; +import { Icon16LinkOutline } from "@vkontakte/icons"; + +interface CitationLinkProps { + url: string; + domain: string; +} + +export const CitationLink: React.FC = ({ url, domain }) => { + const getFaviconUrl = (url: string): string => { + try { + const urlObj = new URL(url); + return `https://www.google.com/s2/favicons?domain=${urlObj.hostname}&sz=32`; + } catch { + return ""; + } + }; + + const faviconUrl = getFaviconUrl(url); + + return ( + } + style={{ + display: "flex", + alignItems: "center", + gap: "8px", + padding: "4px 8px", + borderRadius: "8px", + background: "var(--vkui--color_background)", + border: "1px solid var(--vkui--color_separator_primary)", + textDecoration: "none", + transition: "all 0.2s ease", + }} + > + {faviconUrl && ( + { + (e.target as HTMLImageElement).style.display = "none"; + }} + /> + )} + + {domain} + + + ); +}; diff --git a/GPTutor-Frontend-v2/src/components/CodeCopyButton.tsx b/GPTutor-Frontend-v2/src/components/CodeCopyButton.tsx new file mode 100644 index 00000000..99cda1ac --- /dev/null +++ b/GPTutor-Frontend-v2/src/components/CodeCopyButton.tsx @@ -0,0 +1,56 @@ +import React, { FC, useState } from "react"; +import { Icon16CheckCircle, Icon16CopyOutline } from "@vkontakte/icons"; +import bridge from "@vkontakte/vk-bridge"; + +interface CodeCopyButtonProps { + code: string; + style?: React.CSSProperties; + onCopySuccess?: () => void; + onCopyError?: (error: Error) => void; + successDuration?: number; +} + +export const CodeCopyButton: FC = ({ + code, + style, + onCopySuccess, + onCopyError, + successDuration = 2000, +}) => { + const [isCopied, setIsCopied] = useState(false); + + const handleCopy = async () => { + if (isCopied) return; + + try { + await bridge.send("VKWebAppCopyText", { text: code }); + setIsCopied(true); + onCopySuccess?.(); + + setTimeout(() => setIsCopied(false), successDuration); + } catch (error) { + console.error("CodeCopyButton: Failed to copy code:", error); + onCopyError?.(error as Error); + } + }; + + const buttonStyle: React.CSSProperties = { + ...style, + }; + + const buttonClassName = `code-copy-button ${isCopied ? "copied" : ""}`; + + return ( + + ); +}; + + + diff --git a/GPTutor-Frontend-v2/src/components/Copy.tsx b/GPTutor-Frontend-v2/src/components/Copy.tsx new file mode 100644 index 00000000..1bf1ede6 --- /dev/null +++ b/GPTutor-Frontend-v2/src/components/Copy.tsx @@ -0,0 +1,55 @@ +import { FC, useState } from "react"; +import { Button } from "@vkontakte/vkui"; +import { Icon28CopyOutline } from "@vkontakte/icons"; +import bridge from "@vkontakte/vk-bridge"; + +interface CopyProps { + copyText: string; + textToClickBoard: string; + mode?: "primary" | "secondary" | "tertiary" | "outline" | "link"; + isButton?: boolean; +} + +export const Copy: FC = ({ + copyText, + textToClickBoard, + mode = "secondary", + isButton = false +}) => { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + try { + await bridge.send("VKWebAppCopyText", { text: textToClickBoard }); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error('Failed to copy text: ', err); + } + }; + + if (isButton) { + return ( + + ); + } + + return ( +
+ {copied ? 'Скопировано!' : copyText} +
+ ); +}; + + + + + + diff --git a/GPTutor-Frontend-v2/src/components/CopyButton.tsx b/GPTutor-Frontend-v2/src/components/CopyButton.tsx new file mode 100644 index 00000000..e75a7bc1 --- /dev/null +++ b/GPTutor-Frontend-v2/src/components/CopyButton.tsx @@ -0,0 +1,72 @@ +import React, { FC, useState } from "react"; +import { IconButton } from "@vkontakte/vkui"; +import { Icon16CopyOutline, Icon16CheckCircle } from "@vkontakte/icons"; +import bridge from "@vkontakte/vk-bridge"; + +import classes from "./components.module.css"; + +interface CopyButtonProps { + textToCopy: string; + style?: React.CSSProperties; + onCopySuccess?: () => void; + onCopyError?: (error: Error) => void; + successDuration?: number; + size?: number; +} + +export const CopyButton: FC = ({ + textToCopy, + style, + onCopySuccess, + onCopyError, + successDuration = 1500, + size, +}) => { + const [isCopied, setIsCopied] = useState(false); + + const handleCopy = async () => { + if (isCopied) return; + + try { + await bridge.send("VKWebAppCopyText", { text: textToCopy }); + + setIsCopied(true); + onCopySuccess?.(); + + setTimeout(() => setIsCopied(false), successDuration); + } catch (error) { + onCopyError?.(error as Error); + } + }; + + const buttonStyle: React.CSSProperties = { + borderRadius: "50%", + ...style, + }; + + const buttonClassName = `${classes.copyButton} ${ + isCopied ? classes.copied : "" + }`; + + return ( + + {isCopied ? ( + + ) : ( + + )} + + ); +}; diff --git a/GPTutor-Frontend-v2/src/components/FeatureCard.tsx b/GPTutor-Frontend-v2/src/components/FeatureCard.tsx new file mode 100644 index 00000000..b30b729e --- /dev/null +++ b/GPTutor-Frontend-v2/src/components/FeatureCard.tsx @@ -0,0 +1,40 @@ +import { FC } from 'react'; +import { Card, Cell, Title, Text } from '@vkontakte/vkui'; + +interface FeatureCardProps { + icon: React.ComponentType<{ style?: React.CSSProperties }>; + title: string; + description: string; +} + +export const FeatureCard: FC = ({ icon: Icon, title, description }) => { + return ( + + } + > +
+ + {title} + + + {description} + +
+
+
+ ); +}; + + + + + + + + + + + + + diff --git a/GPTutor-Frontend-v2/src/components/InlineCitation.tsx b/GPTutor-Frontend-v2/src/components/InlineCitation.tsx new file mode 100644 index 00000000..4aa405e1 --- /dev/null +++ b/GPTutor-Frontend-v2/src/components/InlineCitation.tsx @@ -0,0 +1,31 @@ +import React from "react"; + +interface InlineCitationProps { + index: number; + url: string; +} + +/** + * Компонент для отображения inline citation [N] в тексте + */ +export const InlineCitation: React.FC = ({ + index, + url, +}) => { + return ( + + [{index}] + + ); +}; + + + + + diff --git a/GPTutor-Frontend-v2/src/components/ModelCard.tsx b/GPTutor-Frontend-v2/src/components/ModelCard.tsx new file mode 100644 index 00000000..a9b4db15 --- /dev/null +++ b/GPTutor-Frontend-v2/src/components/ModelCard.tsx @@ -0,0 +1,55 @@ +import { FC } from 'react'; +import { Card, Cell, Avatar, Title, Text, Badge } from '@vkontakte/vkui'; + +interface ModelCardProps { + name: string; + provider: string; + tokens: string; + latency: string; + growth: string; +} + +export const ModelCard: FC = ({ name, provider, tokens, latency, growth }) => { + return ( + + } + after={ +
+ + {tokens} токенов/нед + + + {latency} задержка + +
+ } + > +
+ + {name} + + + от {provider} + + + {growth} + +
+
+
+ ); +}; + + + + + + + + + + + + + diff --git a/GPTutor-Frontend-v2/src/components/StatsCard.tsx b/GPTutor-Frontend-v2/src/components/StatsCard.tsx new file mode 100644 index 00000000..35fbcdd9 --- /dev/null +++ b/GPTutor-Frontend-v2/src/components/StatsCard.tsx @@ -0,0 +1,35 @@ +import { FC } from 'react'; +import { Card, Title, Text } from '@vkontakte/vkui'; + +interface StatsCardProps { + icon: React.ComponentType<{ style?: React.CSSProperties }>; + value: string; + label: string; +} + +export const StatsCard: FC = ({ icon: Icon, value, label }) => { + return ( + + + + {value} + + + {label} + + + ); +}; + + + + + + + + + + + + + diff --git a/GPTutor-Frontend-v2/src/components/components.module.css b/GPTutor-Frontend-v2/src/components/components.module.css new file mode 100644 index 00000000..4d7c8ee5 --- /dev/null +++ b/GPTutor-Frontend-v2/src/components/components.module.css @@ -0,0 +1,3 @@ +.copyButton :global(.vkuiIcon ){ + padding: 4px; +} diff --git a/GPTutor-Frontend-v2/src/components/icons/ClaudeIcon.tsx b/GPTutor-Frontend-v2/src/components/icons/ClaudeIcon.tsx new file mode 100644 index 00000000..3b690f92 --- /dev/null +++ b/GPTutor-Frontend-v2/src/components/icons/ClaudeIcon.tsx @@ -0,0 +1,20 @@ +import React from "react"; + +export const ClaudeIcon: React.FC<{ size?: number }> = ({ size = 24 }) => ( + + Claude + + + + +); diff --git a/GPTutor-Frontend-v2/src/components/icons/DeepSeekIcon.tsx b/GPTutor-Frontend-v2/src/components/icons/DeepSeekIcon.tsx new file mode 100644 index 00000000..d84bd321 --- /dev/null +++ b/GPTutor-Frontend-v2/src/components/icons/DeepSeekIcon.tsx @@ -0,0 +1,23 @@ +import React from "react"; + +export const DeepSeekIcon: React.FC<{ size?: number }> = ({ size = 28 }) => ( + + + +); diff --git a/GPTutor-Frontend-v2/src/components/icons/GeminiIcon.tsx b/GPTutor-Frontend-v2/src/components/icons/GeminiIcon.tsx new file mode 100644 index 00000000..5e0f46a3 --- /dev/null +++ b/GPTutor-Frontend-v2/src/components/icons/GeminiIcon.tsx @@ -0,0 +1,115 @@ +import React from 'react'; + +export const GeminiIcon: React.FC<{ size?: number }> = ({ size = 24 }) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); diff --git a/GPTutor-Frontend-v2/src/components/icons/GrokIcon.tsx b/GPTutor-Frontend-v2/src/components/icons/GrokIcon.tsx new file mode 100644 index 00000000..c40dbdd1 --- /dev/null +++ b/GPTutor-Frontend-v2/src/components/icons/GrokIcon.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +export const GrokIcon: React.FC<{ size?: number }> = ({ size = 24 }) => ( + + + +); diff --git a/GPTutor-Frontend-v2/src/components/icons/MistralIcon.tsx b/GPTutor-Frontend-v2/src/components/icons/MistralIcon.tsx new file mode 100644 index 00000000..5f5cd785 --- /dev/null +++ b/GPTutor-Frontend-v2/src/components/icons/MistralIcon.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +export const MistralIcon: React.FC<{ size?: number }> = ({ size = 24 }) => ( + + + + + + + + + + + + + + + + + + + + + +); diff --git a/GPTutor-Frontend-v2/src/components/icons/OpenAIIcon.tsx b/GPTutor-Frontend-v2/src/components/icons/OpenAIIcon.tsx new file mode 100644 index 00000000..8b47baef --- /dev/null +++ b/GPTutor-Frontend-v2/src/components/icons/OpenAIIcon.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +export const OpenAIIcon: React.FC<{ size?: number }> = ({ size = 24 }) => ( + + + +); diff --git a/GPTutor-Frontend-v2/src/components/icons/PerplexityIcon.tsx b/GPTutor-Frontend-v2/src/components/icons/PerplexityIcon.tsx new file mode 100644 index 00000000..e0b96192 --- /dev/null +++ b/GPTutor-Frontend-v2/src/components/icons/PerplexityIcon.tsx @@ -0,0 +1,21 @@ +import React from "react"; + +export const PerplexityIcon: React.FC<{ size?: number }> = ({ size = 24 }) => { + return ( + + + + + + ); +}; diff --git a/GPTutor-Frontend-v2/src/components/icons/QwenIcon.tsx b/GPTutor-Frontend-v2/src/components/icons/QwenIcon.tsx new file mode 100644 index 00000000..ffed6cd0 --- /dev/null +++ b/GPTutor-Frontend-v2/src/components/icons/QwenIcon.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +export const QwenIcon: React.FC<{ size?: number }> = ({ size = 24 }) => ( + + + + + + + + + +); diff --git a/GPTutor-Frontend-v2/src/components/icons/index.ts b/GPTutor-Frontend-v2/src/components/icons/index.ts new file mode 100644 index 00000000..24eccb4a --- /dev/null +++ b/GPTutor-Frontend-v2/src/components/icons/index.ts @@ -0,0 +1,8 @@ +export { GeminiIcon } from "./GeminiIcon"; +export { QwenIcon } from "./QwenIcon"; +export { DeepSeekIcon } from "./DeepSeekIcon"; +export { GrokIcon } from "./GrokIcon"; +export { OpenAIIcon } from "./OpenAIIcon"; +export { MistralIcon } from "./MistralIcon"; +export { PerplexityIcon } from "./PerplexityIcon.tsx"; +export { ClaudeIcon } from "./ClaudeIcon.tsx"; diff --git a/GPTutor-Frontend-v2/src/components/index.ts b/GPTutor-Frontend-v2/src/components/index.ts new file mode 100644 index 00000000..e028debb --- /dev/null +++ b/GPTutor-Frontend-v2/src/components/index.ts @@ -0,0 +1,6 @@ +export { StatsCard } from './StatsCard'; +export { ModelCard } from './ModelCard'; +export { FeatureCard } from './FeatureCard'; +export { AnimatedStatsCard } from './AnimatedStatsCard'; +export { CopyButton } from './CopyButton'; +export { CodeCopyButton } from './CodeCopyButton'; diff --git a/GPTutor-Frontend-v2/src/config/env.ts b/GPTutor-Frontend-v2/src/config/env.ts new file mode 100644 index 00000000..8a35930b --- /dev/null +++ b/GPTutor-Frontend-v2/src/config/env.ts @@ -0,0 +1,13 @@ +// Environment configuration +export const ENV_CONFIG = { + API_URL: import.meta.env.VITE_API_URL || 'https://api.openai.com/v1' +}; + + + + + + + + + diff --git a/GPTutor-Frontend-v2/src/contexts/ThemeContext.tsx b/GPTutor-Frontend-v2/src/contexts/ThemeContext.tsx new file mode 100644 index 00000000..0eba3ef1 --- /dev/null +++ b/GPTutor-Frontend-v2/src/contexts/ThemeContext.tsx @@ -0,0 +1,26 @@ +import { createContext, useContext, ReactNode } from 'react'; + +interface ThemeContextType { + isDarkTheme: boolean; +} + +const ThemeContext = createContext(undefined); + +export const ThemeProvider = ({ children, isDarkTheme }: { + children: ReactNode; + isDarkTheme: boolean; +}) => { + return ( + + {children} + + ); +}; + +export const useTheme = () => { + const context = useContext(ThemeContext); + if (context === undefined) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +}; diff --git a/GPTutor-Frontend-v2/src/eruda.ts b/GPTutor-Frontend-v2/src/eruda.ts new file mode 100644 index 00000000..c8d2ef6f --- /dev/null +++ b/GPTutor-Frontend-v2/src/eruda.ts @@ -0,0 +1,5 @@ +import eruda from "eruda"; + +eruda.init(); + +export default eruda; diff --git a/GPTutor-Frontend-v2/src/hooks/index.ts b/GPTutor-Frontend-v2/src/hooks/index.ts new file mode 100644 index 00000000..85fe64c5 --- /dev/null +++ b/GPTutor-Frontend-v2/src/hooks/index.ts @@ -0,0 +1,7 @@ +export * from './useSnackbar'; +export * from './useCodeCopyButtons'; + + + + + diff --git a/GPTutor-Frontend-v2/src/hooks/useCodeCopyButtons.tsx b/GPTutor-Frontend-v2/src/hooks/useCodeCopyButtons.tsx new file mode 100644 index 00000000..cec5c822 --- /dev/null +++ b/GPTutor-Frontend-v2/src/hooks/useCodeCopyButtons.tsx @@ -0,0 +1,98 @@ +import { useEffect, useRef } from "react"; +import { renderToString } from "react-dom/server"; +import { Button } from "@vkontakte/vkui"; +import { Icon16CopyOutline } from "@vkontakte/icons"; +import bridge from "@vkontakte/vk-bridge"; + +export const useCodeCopyButtons = (isTyping: boolean) => { + const containerRef = useRef(null); + + const renderCopyButton = (code: string, blockId: string) => { + return renderToString( +
+ +
+ ); + }; + + useEffect(() => { + if (!containerRef.current || isTyping) return; + + const addCopyButtons = () => { + const preElements = containerRef.current?.querySelectorAll("pre"); + if (!preElements) return; + + preElements.forEach((preElement, index) => { + const existingButton = + preElement.nextElementSibling?.classList.contains( + "code-copy-button-container" + ); + if (existingButton) return; + + const codeElement = preElement.querySelector("code"); + if (!codeElement) return; + + const cleanCode = + codeElement.textContent || codeElement.innerText || ""; + const blockId = `code-block-${index}`; + + const buttonHtml = renderCopyButton(cleanCode, blockId); + + // Создаем временный контейнер для парсинга HTML + const tempDiv = document.createElement("div"); + tempDiv.innerHTML = buttonHtml; + const buttonContainer = tempDiv.firstElementChild as HTMLElement; + + if (buttonContainer) { + const copyButton = buttonContainer.querySelector( + ".code-copy-button" + ) as HTMLButtonElement; + if (copyButton) { + copyButton.addEventListener("click", async () => { + try { + const codeToCopy = decodeURIComponent( + copyButton.getAttribute("data-code") || "" + ); + await bridge.send("VKWebAppCopyText", { text: codeToCopy }); + } catch (error) { + console.error("Failed to copy code:", error); + } + }); + } + + preElement.parentNode?.insertBefore( + buttonContainer, + preElement.nextSibling + ); + } + }); + }; + + const timer = setTimeout(addCopyButtons, 500); + + return () => clearTimeout(timer); + }, [isTyping]); + + return containerRef; +}; diff --git a/GPTutor-Frontend-v2/src/hooks/useSnackbar.tsx b/GPTutor-Frontend-v2/src/hooks/useSnackbar.tsx new file mode 100644 index 00000000..cd708598 --- /dev/null +++ b/GPTutor-Frontend-v2/src/hooks/useSnackbar.tsx @@ -0,0 +1,100 @@ +import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react'; +import { Snackbar } from '@vkontakte/vkui'; + +export interface SnackbarMessage { + id: string; + text: string; + subtitle?: string; + action?: string; + mode?: 'default' | 'dark'; + duration?: number; + onActionClick?: () => void; +} + +interface SnackbarContextType { + showSnackbar: (message: Omit) => void; + showSuccess: (text: string, subtitle?: string) => void; + showError: (text: string, subtitle?: string) => void; + showInfo: (text: string, subtitle?: string) => void; +} + +const SnackbarContext = createContext(null); + +interface SnackbarProviderProps { + children: ReactNode; +} + +export const SnackbarProvider: React.FC = ({ children }) => { + const [snackbars, setSnackbars] = useState([]); + + const removeSnackbar = useCallback((id: string) => { + setSnackbars(prev => prev.filter(snackbar => snackbar.id !== id)); + }, []); + + const showSnackbar = useCallback((message: Omit) => { + const id = `snackbar_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const snackbarMessage: SnackbarMessage = { + id, + duration: 4000, + ...message, + }; + + setSnackbars(prev => [...prev, snackbarMessage]); + }, []); + + const showSuccess = useCallback((text: string, subtitle?: string) => { + showSnackbar({ + text, + subtitle, + }); + }, [showSnackbar]); + + const showError = useCallback((text: string, subtitle?: string) => { + showSnackbar({ + text, + subtitle, + duration: 6000, // Ошибки показываем дольше + }); + }, [showSnackbar]); + + const showInfo = useCallback((text: string, subtitle?: string) => { + showSnackbar({ + text, + subtitle, + }); + }, [showSnackbar]); + + const contextValue: SnackbarContextType = { + showSnackbar, + showSuccess, + showError, + showInfo, + }; + + return ( + + {children} + {snackbars.map((snackbar) => ( + removeSnackbar(snackbar.id)} + duration={snackbar.duration} + mode={snackbar.mode} + action={snackbar.action} + onActionClick={snackbar.onActionClick} + subtitle={snackbar.subtitle} + > + {snackbar.text} + + ))} + + ); +}; + +export const useSnackbar = (): SnackbarContextType => { + const context = useContext(SnackbarContext); + if (!context) { + throw new Error('useSnackbar must be used within a SnackbarProvider'); + } + return context; +}; diff --git a/GPTutor-Frontend-v2/src/main.tsx b/GPTutor-Frontend-v2/src/main.tsx new file mode 100644 index 00000000..6205cabb --- /dev/null +++ b/GPTutor-Frontend-v2/src/main.tsx @@ -0,0 +1,12 @@ +import { createRoot } from 'react-dom/client'; +import vkBridge from '@vkontakte/vk-bridge'; +import { AppConfig } from './AppConfig.tsx'; + +vkBridge.send('VKWebAppInit'); + + +createRoot(document.getElementById('root')!).render(); + +if (import.meta.env.MODE === 'development') { + import('./eruda.ts'); +} diff --git a/GPTutor-Frontend-v2/src/modals/TopUpBalanceModal.tsx b/GPTutor-Frontend-v2/src/modals/TopUpBalanceModal.tsx new file mode 100644 index 00000000..43b3719f --- /dev/null +++ b/GPTutor-Frontend-v2/src/modals/TopUpBalanceModal.tsx @@ -0,0 +1,164 @@ +import { FC, useState } from "react"; +import { + Button, + FormItem, + Input, + ModalPage, + ModalPageHeader, + PanelHeaderClose, + Spacing, + Spinner, +} from "@vkontakte/vkui"; +import { useRouteNavigator } from "@vkontakte/vk-mini-apps-router"; +import { paymentApi } from "../api"; + +interface TopUpBalanceModalProps { + id: string; +} + +export const TopUpBalanceModal: FC = ({ id }) => { + const routeNavigator = useRouteNavigator(); + const [amount, setAmount] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const handleClose = () => { + routeNavigator.hideModal(); + }; + + const handleAmountChange = (e: React.ChangeEvent) => { + const value = e.target.value; + // Разрешаем только числа и точку + if (value === "" || /^\d*\.?\d*$/.test(value)) { + setAmount(value); + setError(""); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + const numAmount = parseFloat(amount); + + if (!amount || isNaN(numAmount)) { + setError("Введите сумму пополнения"); + return; + } + + if (numAmount <= 0) { + setError("Сумма должна быть больше 0"); + return; + } + + if (numAmount < 70) { + setError("Минимальная сумма пополнения: 70₽"); + return; + } + + if (numAmount > 100000) { + setError("Максимальная сумма пополнения: 100,000₽"); + return; + } + + setLoading(true); + setError(""); + + try { + const response = await paymentApi.createPayment({ + amount: numAmount, + description: `Пополнение баланса на ${numAmount}₽`, + returnUrl: window.location.origin + "/profile", + }); + + console.log("Платеж создан:", response); + + // Открываем страницу оплаты ЮКассы в новой вкладке + if (response.payment.confirmationUrl) { + window.open(response.payment.confirmationUrl, "_blank"); + handleClose(); + } else { + setError("Не удалось получить ссылку для оплаты"); + } + } catch (err) { + console.error("Ошибка создания платежа:", err); + setError( + err instanceof Error ? err.message : "Не удалось создать платеж" + ); + } finally { + setLoading(false); + } + }; + + const quickAmounts = [100, 500, 1000, 5000]; + + return ( + }> + Пополнить баланс + + } + > +
+ + + + + +
+ {quickAmounts.map((quickAmount) => ( + + ))} +
+
+ + + + + + + +
+ ); +}; diff --git a/GPTutor-Frontend-v2/src/modals/index.ts b/GPTutor-Frontend-v2/src/modals/index.ts new file mode 100644 index 00000000..1d7f5f8e --- /dev/null +++ b/GPTutor-Frontend-v2/src/modals/index.ts @@ -0,0 +1,2 @@ +export { TopUpBalanceModal } from "./TopUpBalanceModal"; + diff --git a/GPTutor-Frontend-v2/src/panels/Chat/Chat.tsx b/GPTutor-Frontend-v2/src/panels/Chat/Chat.tsx new file mode 100644 index 00000000..9afcc943 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Chat/Chat.tsx @@ -0,0 +1,120 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { NavIdProps, Panel, ScrollArrow } from "@vkontakte/vkui"; +import { observer } from "mobx-react-lite"; +import { userViewModel } from "../../viewModels/UserViewModel"; +import { chatViewModel } from "./models"; +import { ChatHeader, ChatInput, MessageList } from "./components"; +import { + useChatHandlers, + useChatNavigation, + useMessengerScroll, +} from "./hooks"; +import { + chatContainerStyle, + messagesInnerContainerStyle, + messagesScrollContainerStyle, + scrollArrowContainerStyle, +} from "./styles"; +import { useSnackbar } from "../../hooks"; + +export interface ChatProps extends NavIdProps {} + +/** + * Главная панель чата + */ +export const Chat: React.FC = observer(({ id }) => { + const [message, setMessage] = useState(""); + const messagesEndRef = useRef(null); + + // Хуки + const { scrollRef, scrollToBottom, showScrollDown } = useMessengerScroll( + chatViewModel.isTyping + ); + const { handleBack, handleModelSelect } = useChatNavigation(); + const { handleSendMessage, handleStartChat } = useChatHandlers({ + scrollToBottom, + onMessageChange: setMessage, + }); + const { showError } = useSnackbar(); + + // Инициализация пользователя и snackbar callback + useEffect(() => { + userViewModel.getUser(); + chatViewModel.setSnackbarCallback(showError); + }, [showError]); + + // Получение имени пользователя + const getUserName = useCallback(() => { + return userViewModel.user?.first_name || "Вы"; + }, [userViewModel.user?.first_name]); + + // Очистка сообщений + const handleClearMessages = useCallback(() => { + chatViewModel.clearMessages(); + }, []); + + // Обработчики файлов + const handleFileUpload = useCallback((file: File) => { + console.log("Starting file upload:", file.name); + // Не ждем завершения загрузки, чтобы можно было загружать несколько файлов параллельно + chatViewModel.uploadFile(file).catch((error) => { + console.error("Failed to upload file:", error); + }); + }, []); + + const handleFileRemove = useCallback((fileId: string) => { + chatViewModel.removeFile(fileId); + }, []); + + const handleCancelUpload = useCallback((uploadId: string) => { + chatViewModel.cancelUpload(uploadId); + }, []); + + const handleOnlineModeToggle = useCallback(() => { + chatViewModel.toggleOnlineMode(); + }, []); + + return ( + +
+ + +
+
+ 0} + /> +
+
+ +
+ {showScrollDown && ( + + )} +
+ + handleSendMessage(message)} + disabled={chatViewModel.isLoading} + currentModel={chatViewModel.currentModel} + isOnlineMode={chatViewModel.isOnlineMode} + onModelSelect={handleModelSelect} + onOnlineModeToggle={handleOnlineModeToggle} + onClearMessages={handleClearMessages} + attachedFiles={chatViewModel.getAttachedFiles()} + uploadingFiles={chatViewModel.getUploadingFiles()} + onFileUpload={handleFileUpload} + onFileRemove={handleFileRemove} + onCancelUpload={handleCancelUpload} + /> +
+
+ ); +}); diff --git a/GPTutor-Frontend-v2/src/panels/Chat/README.md b/GPTutor-Frontend-v2/src/panels/Chat/README.md new file mode 100644 index 00000000..4cd1943f --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Chat/README.md @@ -0,0 +1,162 @@ +# Chat Panel + +Панель чата для общения с AI ассистентом Giga Router. + +## Структура + +``` +Chat/ +├── components/ # React компоненты +│ ├── ChatHeader.tsx # Заголовок чата +│ ├── ChatInput.tsx # Поле ввода сообщения +│ ├── MessageItem.tsx # Отдельное сообщение +│ ├── MessageList.tsx # Список сообщений +│ └── index.ts # Экспорты компонентов +├── hooks/ # Кастомные хуки +│ ├── useChatHandlers.ts # Обработчики действий чата +│ ├── useChatNavigation.ts # Навигация в чате +│ ├── useMessengerScroll.ts # Автоматический скролл +│ └── index.ts # Экспорты хуков +├── models/ # Модели данных +│ ├── ChatViewModel.ts # ViewModel управления чатом +│ ├── MessageModel.ts # Модель сообщения +│ └── index.ts # Экспорты моделей +├── Chat.tsx # Главный компонент панели +├── constants.ts # Константы (текста, размеры, URL) +├── styles.ts # Стили компонентов +├── types.ts # TypeScript типы и интерфейсы +└── index.ts # Экспорт панели + +``` + +## Основные возможности + +- ✅ Потоковая генерация ответов (streaming) +- ✅ Отображение статистики токенов и стоимости +- ✅ Автоматический скролл при печатании +- ✅ Skeleton загрузки для сообщений +- ✅ Копирование сообщений +- ✅ Подсветка кода в markdown +- ✅ Выбор модели AI + +## Компоненты + +### Chat +Главный компонент панели. Координирует работу всех подкомпонентов. + +### ChatHeader +Отображает информацию о боте: +- Аватар +- Название (Giga Router) +- Статус (печатает / текущая модель) + +### MessageList +Список сообщений с двумя состояниями: +- Пустое состояние - плейсхолдер с кнопкой запуска +- Список сообщений + +### MessageItem +Отдельное сообщение с: +- Аватаром отправителя +- Содержимым (с поддержкой markdown) +- Кнопкой копирования +- Статистикой токенов (для ассистента) + +### ChatInput +Поле ввода с: +- Кнопкой отправки +- Выбором модели +- Блокировкой при загрузке + +## Хуки + +### useMessengerScroll +Управляет автоматическим скроллом: +- Скролл во время печатания +- Кнопка "прокрутить вниз" +- Блокировка скролла при касании пользователя + +### useChatHandlers +Обработчики действий: +- Отправка сообщений +- Запуск приветственного диалога +- Копирование текста + +### useChatNavigation +Навигация: +- Возврат назад +- Переход к выбору модели + +## Модели + +### ChatViewModel (MobX) +Управляет состоянием чата: +- `messages` - список сообщений +- `isTyping` - флаг печатания +- `isLoading` - флаг загрузки +- `currentModel` - текущая модель +- `error` - ошибка + +**Методы:** +- `sendMessage()` - отправить сообщение +- `startWelcomeChat()` - начать диалог +- `clearMessages()` - очистить сообщения +- `setModel()` - изменить модель + +### MessageModel (MobX) +Модель сообщения: +- `id` - уникальный идентификатор +- `role` - роль (user/assistant/system) +- `content` - текст сообщения +- `isTyping` - флаг печатания +- `timestamp` - время создания +- `usage` - статистика токенов + +## Константы + +Все магические значения вынесены в `constants.ts`: +- Размеры (высота хедера, отступы) +- Задержки (скролл) +- Тексты UI +- URL изображений +- Z-индексы + +## Стили + +Стили вынесены в `styles.ts` для переиспользования: +- `chatContainerStyle` +- `chatHeaderStyle` +- `messagesScrollContainerStyle` +- `scrollArrowContainerStyle` + +## Использование + +```tsx +import { Chat } from "./panels/Chat"; + + +``` + +## API + +Чат использует API endpoint: +``` +POST /v1/chat/completions +``` + +С поддержкой: +- Streaming ответов (SSE) +- Статистики токенов +- Нескольких моделей + +## Особенности реализации + +1. **Streaming** - ответы приходят потоком через SSE +2. **MobX** - реактивное управление состоянием +3. **Оптимизация** - мемоизация колбэков через useCallback +4. **Типизация** - полная типизация TypeScript +5. **Комментарии** - JSDoc для всех публичных методов + + + + diff --git a/GPTutor-Frontend-v2/src/panels/Chat/components/AttachedFiles.css b/GPTutor-Frontend-v2/src/panels/Chat/components/AttachedFiles.css new file mode 100644 index 00000000..c685361b --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Chat/components/AttachedFiles.css @@ -0,0 +1,274 @@ +.attached-files-container { + position: relative; + display: flex; + flex-wrap: wrap; + gap: 12px; + padding: var(--vkui--size_base_padding_vertical--regular); + background: var(--vkui--color_background_contrast_themed); + border-bottom: 1px solid var(--vkui--color_separator_primary); + transition: all 0.2s ease; +} + +.attached-files-container.drag-over { + background: var(--vkui--color_background_secondary); + border: 2px dashed var(--vkui--color_accent_blue); + border-radius: 8px; +} + +.attached-file-item { + position: relative; +} + +.file-preview { + position: relative; + border-radius: 8px; + background: var(--vkui--color_background); + border: 1px solid var(--vkui--color_separator_primary); +} + +.file-preview img { + border-radius: 8px; + +} +.square-preview { + max-height: 88px; + max-width: 88px; + min-width: 88px; + min-height: 88px; + width: 88px; + height: 88px; + display: flex; + align-items: center; + justify-content: center; +} + +.file-image { + width: 100%; + height: 100%; + object-fit: cover; +} + +.file-extension { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + border-radius: 8px; +} + +.file-content { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-start; + width: 100%; + height: 100%; + padding: 6px; + box-sizing: border-box; +} + +.extension-text { + font-size: 16px; + font-weight: 800; + color: white; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4); + letter-spacing: 1px; + line-height: 1; + align-self: flex-start; +} + +.file-details { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 1px; + align-self: flex-start; +} + +.file-name { + font-size: 11px; + font-weight: 600; + color: white; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4); + line-height: 1.1; + text-align: left; + word-break: break-all; + max-width: 100%; +} + +.file-size { + font-size: 9px; + font-weight: 500; + color: rgba(255, 255, 255, 0.9); + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4); + line-height: 1.1; +} + +/* Цвета для разных типов файлов */ +.file-type-pdf { + background: linear-gradient(135deg, #e53e3e, #c53030); +} + +.file-type-txt { + background: linear-gradient(135deg, #718096, #4a5568); +} + +.file-type-js { + background: linear-gradient(135deg, #f6e05e, #d69e2e); +} + +.file-type-json { + background: linear-gradient(135deg, #68d391, #38a169); +} + +.file-type-doc { + background: linear-gradient(135deg, #3182ce, #2c5282); +} + +.file-type-xls { + background: linear-gradient(135deg, #38a169, #2f855a); +} + +.file-type-html { + background: linear-gradient(135deg, #ed8936, #dd6b20); +} + +.file-type-css { + background: linear-gradient(135deg, #9f7aea, #805ad5); +} + +.file-type-audio { + background: linear-gradient(135deg, #805ad5, #6b46c1); +} + +.file-type-video { + background: linear-gradient(135deg, #ed64a6, #d53f8c); +} + +.file-type-archive { + background: linear-gradient(135deg, #4a5568, #2d3748); +} + +.file-type-image { + background: linear-gradient(135deg, #38b2ac, #319795); +} + +.file-type-default { + background: linear-gradient(135deg, #a0aec0, #718096); +} + +.remove-file-btn { + position: absolute; + top: -6px; + right: -6px; + width: 20px; + height: 20px; + min-width: 20px; + padding: 0; + background: #6b7280; + border-radius: 50%; + border: 1px solid #4b5563; + color: white; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 2; + opacity: 0.9; + transition: all 0.2s ease; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); +} + +.remove-file-btn:hover { + background: #4b5563; + opacity: 1; + transform: scale(1.1); + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.3); +} + +.remove-file-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +.remove-file-btn:active { + transform: scale(0.9); +} + +.file-preview.uploading { + opacity: 0.7; +} + +.file-extension.uploading { + position: relative; +} + +.upload-progress { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0, 0, 0, 0.6); + border-radius: 4px; + padding: 4px; + display: flex; + align-items: center; + justify-content: center; +} + +.upload-progress .vkuiSpinner { + color: white; +} + +/* Image Loader */ +.image-loader { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + background: var(--vkui--color_background_secondary); +} + +/* Drop Zone Overlay */ +.drop-zone-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 122, 255, 0.1); + backdrop-filter: blur(2px); + display: flex; + align-items: center; + justify-content: center; + border-radius: 8px; + z-index: 10; + pointer-events: none; +} + +.drop-zone-content { + background: var(--vkui--color_accent_blue); + color: white; + padding: 16px 24px; + border-radius: 12px; + font-size: 16px; + font-weight: 600; + box-shadow: 0 4px 16px rgba(0, 122, 255, 0.3); + animation: dropZonePulse 1s ease-in-out infinite; +} + +@keyframes dropZonePulse { + 0%, 100% { + transform: scale(1); + } + 50% { + transform: scale(1.05); + } +} diff --git a/GPTutor-Frontend-v2/src/panels/Chat/components/AttachedFiles.tsx b/GPTutor-Frontend-v2/src/panels/Chat/components/AttachedFiles.tsx new file mode 100644 index 00000000..0e0a8f6f --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Chat/components/AttachedFiles.tsx @@ -0,0 +1,170 @@ +import React, { useState, useEffect } from "react"; +import { Div, DropZone, Placeholder } from "@vkontakte/vkui"; +import { Icon28DocumentPlusOutline } from "@vkontakte/icons"; +import { FileInfo } from "../../../api"; +import { UploadingFile } from "../types"; +import { FileDisplay } from "./FileDisplay"; +import "./AttachedFiles.css"; +import { observer } from "mobx-react-lite"; + +interface AttachedFilesProps { + files: FileInfo[]; + uploadingFiles?: UploadingFile[]; + onFileRemove: (fileId: string) => void; + onCancelUpload?: (uploadId: string) => void; + onFileSelect?: (file: File) => void; + disabled?: boolean; +} + +export const AttachedFiles: React.FC = observer( + ({ + files, + uploadingFiles = [], + onFileRemove, + onCancelUpload, + onFileSelect, + disabled = false, + }) => { + const [isDraggingOverWindow, setIsDraggingOverWindow] = useState(false); + + console.log("AttachedFiles render:", { + files: files.length, + uploadingFiles: uploadingFiles.length, + }); + + useEffect(() => { + let dragCounter = 0; + + const handleWindowDragEnter = (e: DragEvent) => { + dragCounter++; + if (e.dataTransfer?.types.includes("Files")) { + setIsDraggingOverWindow(true); + } + }; + + const handleWindowDragLeave = () => { + dragCounter--; + if (dragCounter === 0) { + setIsDraggingOverWindow(false); + } + }; + + const handleWindowDrop = () => { + dragCounter = 0; + setIsDraggingOverWindow(false); + }; + + const handleWindowDragOver = (e: DragEvent) => { + e.preventDefault(); + }; + + window.addEventListener("dragenter", handleWindowDragEnter); + window.addEventListener("dragleave", handleWindowDragLeave); + window.addEventListener("drop", handleWindowDrop); + window.addEventListener("dragover", handleWindowDragOver); + + return () => { + window.removeEventListener("dragenter", handleWindowDragEnter); + window.removeEventListener("dragleave", handleWindowDragLeave); + window.removeEventListener("drop", handleWindowDrop); + window.removeEventListener("dragover", handleWindowDragOver); + }; + }, []); + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + setIsDraggingOverWindow(false); + + if (disabled || !onFileSelect) return; + + const droppedFiles = e.dataTransfer.files; + if (droppedFiles && droppedFiles.length > 0) { + Array.from(droppedFiles).forEach((file) => { + onFileSelect(file); + }); + } + }; + + const hasFiles = files.length > 0 || uploadingFiles.length > 0; + + if (!hasFiles && !isDraggingOverWindow) { + return null; + } + + if (!hasFiles && isDraggingOverWindow) { + return ( + { + event.preventDefault(); + }} + onDrop={handleDrop} + > + {({ active }) => ( + + } + title="Загрузка файлов" + > + {active ? "Отпустите файлы здесь." : "Перенесите файл сюда."} + + )} + + ); + } + + return ( + { + event.preventDefault(); + }} + onDrop={handleDrop} + > + {({ active }) => ( + <> + {active ? ( + + } + title="Загрузка файлов" + > + {active ? "Отпустите файлы здесь." : "Перенесите файл сюда."} + + ) : ( +
+ {files.map((file) => ( + onFileRemove(file.id)} + disabled={disabled} + /> + ))} + + {uploadingFiles.map((uploadingFile) => ( + onCancelUpload?.(uploadingFile.id)} + disabled={disabled} + /> + ))} +
+ )} + + )} +
+ ); + } +); diff --git a/GPTutor-Frontend-v2/src/panels/Chat/components/ChatHeader.tsx b/GPTutor-Frontend-v2/src/panels/Chat/components/ChatHeader.tsx new file mode 100644 index 00000000..bce6e195 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Chat/components/ChatHeader.tsx @@ -0,0 +1,86 @@ +import React from "react"; +import { Avatar, Text, IconButton } from "@vkontakte/vkui"; +import { + Icon12OnlineMobile, + Icon24ArrowLeftOutline, + Icon28MessageOutline, +} from "@vkontakte/icons"; +import { observer } from "mobx-react-lite"; +import { ChatHeaderProps } from "../types"; +import { chatHeaderStyle } from "../styles"; +import { CHAT_TITLE, TYPING_TEXT, BOT_AVATAR_URL } from "../constants"; + +export const ChatHeader: React.FC = observer( + ({ isTyping, onBack }) => { + return ( +
+ + + + + } + style={{ + backgroundColor: "var(--vkui--color_accent_blue)", + marginRight: "12px", + }} + /> + +
+ + {CHAT_TITLE} + + +
+ {isTyping ? ( + + {TYPING_TEXT} + + ) : ( + <> + + Онлайн + + + + )} +
+
+
+ ); + } +); diff --git a/GPTutor-Frontend-v2/src/panels/Chat/components/ChatInput.tsx b/GPTutor-Frontend-v2/src/panels/Chat/components/ChatInput.tsx new file mode 100644 index 00000000..d9f69be7 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Chat/components/ChatInput.tsx @@ -0,0 +1,140 @@ +import React from "react"; +import { + Button, + Div, + Flex, + Separator, + WriteBar, + WriteBarIcon, +} from "@vkontakte/vkui"; +import { + Icon24DeleteOutline, + Icon28Send, + Icon28SettingsOutline, +} from "@vkontakte/icons"; +import { observer } from "mobx-react-lite"; +import { ChatInputProps } from "../types"; +import { AttachedFiles } from "./AttachedFiles"; +import { FileUpload } from "./FileUpload"; + +export const ChatInput: React.FC = observer( + ({ + message, + onMessageChange, + onSendMessage, + disabled = false, + currentModel, + onModelSelect, + onClearMessages, + attachedFiles = [], + uploadingFiles = [], + onFileUpload, + onFileRemove, + onCancelUpload, + }) => { + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + // Блокируем отправку если загружаются файлы + if (uploadingFiles && uploadingFiles.length > 0) { + return; + } + onSendMessage(); + } + }; + + const handleFileUpload = (file: File) => { + if (onFileUpload) { + onFileUpload(file); + } + }; + + const handleFileRemove = (fileId: string) => { + if (onFileRemove) { + onFileRemove(fileId); + } + }; + + return ( +
+
+ + + +
+ + + + + + onMessageChange(e.target.value)} + onKeyDown={handleKeyDown} + disabled={disabled} + before={ + + } + after={ + <> + + + + 0) + } + title={ + uploadingFiles && uploadingFiles.length > 0 + ? "Дождитесь загрузки файлов" + : undefined + } + > + + + + } + placeholder="Сообщение" + /> +
+ ); + } +); diff --git a/GPTutor-Frontend-v2/src/panels/Chat/components/FileDisplay.tsx b/GPTutor-Frontend-v2/src/panels/Chat/components/FileDisplay.tsx new file mode 100644 index 00000000..97199240 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Chat/components/FileDisplay.tsx @@ -0,0 +1,147 @@ +import React from "react"; +import { IconButton, Spinner } from "@vkontakte/vkui"; +import { Icon20Clear } from "@vkontakte/icons"; +import { FileInfo } from "../../../api"; +import { UploadingFile } from "../types"; +import "./AttachedFiles.css"; + +interface FileDisplayProps { + file?: FileInfo; + uploadingFile?: UploadingFile; + showRemoveButton?: boolean; + onRemove?: () => void; + disabled?: boolean; +} + +export const FileDisplay: React.FC = ({ + file, + uploadingFile, + showRemoveButton = false, + onRemove, + disabled = false, +}) => { + const currentFile = file || uploadingFile?.file; + const isUploading = !!uploadingFile; + + if (!currentFile) { + return null; + } + + const isImage = currentFile.type.startsWith("image/"); + + const getFileExtension = (filename: string) => { + const extension = filename.split(".").pop()?.toUpperCase() || "FILE"; + + if (extension.length > 4) { + return extension.substring(0, 4); + } + + return extension; + }; + + const getTruncatedFileName = (filename: string) => { + const nameWithoutExt = filename.replace(/\.[^/.]+$/, ""); + + if (nameWithoutExt.length > 10) { + return nameWithoutExt.substring(0, 10) + "..."; + } + + return nameWithoutExt; + }; + + const formatFileSize = (bytes: number) => { + if (bytes === 0) return "0 B"; + const k = 1024; + const sizes = ["B", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i]; + }; + + const getFileTypeClass = (type: string, filename: string) => { + if (type.startsWith("image/")) { + return "file-type-image"; + } else if (type === "application/pdf") { + return "file-type-pdf"; + } else if (type.includes("text/") || filename.endsWith(".txt")) { + return "file-type-txt"; + } else if (type.includes("javascript") || filename.endsWith(".js")) { + return "file-type-js"; + } else if (type.includes("json") || filename.endsWith(".json")) { + return "file-type-json"; + } else if (type.includes("audio/")) { + return "file-type-audio"; + } else if (type.includes("video/")) { + return "file-type-video"; + } else if (type.includes("zip") || type.includes("rar")) { + return "file-type-archive"; + } else if (filename.endsWith(".doc") || filename.endsWith(".docx")) { + return "file-type-doc"; + } else if (filename.endsWith(".xls") || filename.endsWith(".xlsx")) { + return "file-type-xls"; + } else if (filename.endsWith(".html") || filename.endsWith(".htm")) { + return "file-type-html"; + } else if (filename.endsWith(".css")) { + return "file-type-css"; + } else { + return "file-type-default"; + } + }; + + return ( +
+
+ {isImage ? ( + <> + {isUploading ? ( +
+ +
+ ) : ( + {currentFile.name} + )} + + ) : ( +
+
+ + {getFileExtension(currentFile.name)} + +
+ + {getTruncatedFileName(currentFile.name)} + + + {formatFileSize(currentFile.size)} + +
+ {isUploading && ( +
+ +
+ )} +
+
+ )} + {showRemoveButton && onRemove && ( +
+ + + +
+ )} +
+
+ ); +}; diff --git a/GPTutor-Frontend-v2/src/panels/Chat/components/FileUpload.css b/GPTutor-Frontend-v2/src/panels/Chat/components/FileUpload.css new file mode 100644 index 00000000..e2557cf3 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Chat/components/FileUpload.css @@ -0,0 +1,64 @@ +.file-upload-container { + position: relative; + display: flex; + align-items: center; +} + +.file-upload-button { + color: var(--vkui--color_icon_secondary); + transition: color 0.2s ease; +} + +.file-upload-button:hover:not(:disabled) { + color: var(--vkui--color_icon_accent); +} + +.file-upload-container.disabled .file-upload-button { + opacity: 0.5; + cursor: not-allowed; +} + +.file-upload-button[data-write-bar-icon] { + cursor: pointer; +} + +.file-upload-button[data-write-bar-icon]:disabled { + cursor: not-allowed; +} + +.drag-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + pointer-events: none; +} + +.drag-overlay-content { + background: var(--vkui--color_background); + padding: 24px; + border-radius: 12px; + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + border: 2px dashed var(--vkui--color_icon_accent); +} + +.drag-overlay-content svg { + color: var(--vkui--color_icon_accent); + font-size: 48px; +} + +.drag-overlay-content span { + color: var(--vkui--color_text_primary); + font-size: 16px; + font-weight: 500; +} diff --git a/GPTutor-Frontend-v2/src/panels/Chat/components/FileUpload.tsx b/GPTutor-Frontend-v2/src/panels/Chat/components/FileUpload.tsx new file mode 100644 index 00000000..ed82c693 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Chat/components/FileUpload.tsx @@ -0,0 +1,105 @@ +import React, { useRef, useState, useCallback } from "react"; +import { WriteBarIcon } from "@vkontakte/vkui"; +import { Icon28PictureOutline } from "@vkontakte/icons"; +import "./FileUpload.css"; + +interface FileUploadProps { + onFileSelect: (file: File) => void; + disabled?: boolean; + accept?: string; + multiple?: boolean; +} + +export const FileUpload: React.FC = ({ + onFileSelect, + disabled = false, + accept = "*/*", + multiple = true, +}) => { + const fileInputRef = useRef(null); + const [isDragOver, setIsDragOver] = useState(false); + + const handleFileSelect = useCallback((files: FileList | null) => { + if (!files || files.length === 0) return; + + Array.from(files).forEach(file => { + onFileSelect(file); + }); + }, [onFileSelect]); + + const handleInputChange = (event: React.ChangeEvent) => { + handleFileSelect(event.target.files); + // Очищаем input для возможности повторного выбора того же файла + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + const handleButtonClick = () => { + if (fileInputRef.current) { + fileInputRef.current.click(); + } + }; + + const handleDragOver = useCallback((event: React.DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + if (!disabled) { + setIsDragOver(true); + } + }, [disabled]); + + const handleDragLeave = useCallback((event: React.DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + setIsDragOver(false); + }, []); + + const handleDrop = useCallback((event: React.DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + setIsDragOver(false); + + if (disabled) return; + + const files = event.dataTransfer.files; + handleFileSelect(files); + }, [disabled, handleFileSelect]); + + return ( +
+ + + + + + {isDragOver && ( +
+
+ + Отпустите файлы для загрузки +
+
+ )} +
+ ); +}; diff --git a/GPTutor-Frontend-v2/src/panels/Chat/components/MessageItem.tsx b/GPTutor-Frontend-v2/src/panels/Chat/components/MessageItem.tsx new file mode 100644 index 00000000..a71062e8 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Chat/components/MessageItem.tsx @@ -0,0 +1,555 @@ +import React, { useMemo, useState } from "react"; +import { Avatar, Button, Skeleton, Spinner, Text } from "@vkontakte/vkui"; +import { + Icon16ArrowDownOutline, + Icon16ArrowUpOutline, + Icon24MoneyCircleOutline, + Icon28MessageOutline, + Icon28MoneyCircleOutline, + Icon28UserCircleOutline, +} from "@vkontakte/icons"; +import { MessageItemProps } from "../types"; +import Markdown from "../../../services/Markdown"; +import { useCodeCopyButtons } from "../../../hooks"; +import { FileDisplay } from "./FileDisplay"; +import { CitationLink } from "../../../components/CitationLink"; +import { + formatTextWithCitations, + getDomainFromUrl, +} from "../../../utils/citationFormatter"; +import { observer } from "mobx-react-lite"; +import { useRouteNavigator } from "@vkontakte/vk-mini-apps-router"; +import { DEFAULT_VIEW_PANELS } from "../../../routes.ts"; +import { CopyButton } from "../../../components"; +import bridge from "@vkontakte/vk-bridge"; +import { filesApi } from "../../../api"; + +const markdown = new Markdown(); + +export const MessageItem: React.FC = observer( + ({ message, userViewModel, getUserName }) => { + const containerRef = useCodeCopyButtons(message.isTyping); + const routeNavigator = useRouteNavigator(); + + // Состояние загрузки изображений: imageIndex -> uploading + const [uploadingImages, setUploadingImages] = useState< + Record + >({}); + + // Проверяем, это ошибка недостаточного баланса + const isInsufficientBalance = message.content.includes( + "Недостаточно средств на балансе" + ); + + // Проверка, является ли URL base64 + const isBase64Image = (url: string): boolean => { + return url.startsWith("data:image/"); + }; + + // Функция для конвертации base64 в File + const base64ToFile = (base64String: string, fileName: string): File => { + // Извлекаем mime type и данные из base64 + const arr = base64String.split(","); + const mimeMatch = arr[0].match(/:(.*?);/); + const mime = mimeMatch ? mimeMatch[1] : "image/png"; + const bstr = atob(arr[1]); + let n = bstr.length; + const u8arr = new Uint8Array(n); + while (n--) { + u8arr[n] = bstr.charCodeAt(n); + } + return new File([u8arr], fileName, { type: mime }); + }; + + // Функция для загрузки base64 изображения на сервер и получения URL + const uploadImageToServer = async ( + base64Image: string, + imageIndex: number + ): Promise => { + try { + setUploadingImages((prev) => ({ + ...prev, + [imageIndex]: true, + })); + + const mimeMatch = base64Image.match(/data:(.*?);/); + const mime = mimeMatch ? mimeMatch[1] : "image/png"; + const extension = mime.split("/")[1] || "png"; + const fileName = `generated_image_${Date.now()}.${extension}`; + + const file = base64ToFile(base64Image, fileName); + + const uploadResult = await filesApi.uploadFile(file); + const serverUrl = uploadResult.file.url; + + message.updateGeneratedImageUrl(imageIndex, serverUrl); + + setUploadingImages((prev) => ({ + ...prev, + [imageIndex]: false, + })); + + return serverUrl; + } catch (error) { + console.error("Error uploading image:", error); + setUploadingImages((prev) => ({ + ...prev, + [imageIndex]: false, + })); + + // Показываем ошибку через bridge + bridge.send("VKWebAppTapticNotificationOccurred", { type: "error" }); + return null; + } + }; + + // Функция для показа изображения (загружает на сервер если нужно) + const handleImageClick = async (imageUrl: string, imageIndex: number) => { + // Если сейчас загружается, ничего не делаем + if (uploadingImages[imageIndex]) { + return; + } + + // Если это не base64 (уже серверная ссылка), показываем сразу + if (!isBase64Image(imageUrl)) { + bridge + .send("VKWebAppShowImages", { + images: [imageUrl], + }) + .then((data) => { + console.log(data); + }) + .catch((error) => { + console.log(error); + }); + return; + } + + // Это base64 - загружаем на сервер и показываем + const serverUrl = await uploadImageToServer(imageUrl, imageIndex); + if (serverUrl) { + bridge + .send("VKWebAppShowImages", { + images: [serverUrl], + }) + .then((data) => { + console.log(data); + }) + .catch((error) => { + console.log(error); + }); + } + }; + + // Форматируем контент с цитированиями + const formattedContent = useMemo(() => { + const renderedMarkdown = markdown.render(message.content); + + if (message.citations && message.citations.length > 0) { + console.log("Formatting content with citations:", { + citationsCount: message.citations.length, + citations: message.citations, + contentLength: message.content.length, + }); + const formatted = formatTextWithCitations( + renderedMarkdown, + message.citations + ); + console.log("Formatted content preview:", formatted.substring(0, 500)); + return formatted; + } + + return renderedMarkdown; + }, [message.content, message.citations]); + + // Обработчик перехода на профиль + + return ( +
+
+ +
+
+
+ + ) : ( + + ) + } + style={{ + backgroundColor: message.isAssistant + ? "var(--vkui--color_icon_secondary)" + : "var(--vkui--color_accent_blue)", + }} + /> +
+ +
+
+ + {message.isAssistant ? "Giga Router" : getUserName()} + +
+ +
+ {message.isTyping ? ( +
+ + + +
+ ) : ( + <> + {/* Отображение прикрепленных файлов */} + {message.attachedFilesList && + message.attachedFilesList.length > 0 && ( +
+ {message.attachedFilesList.map((file) => ( + + ))} +
+ )} + + {/* Отображение сгенерированных изображений */} + {message.generatedImages && + message.generatedImages.length > 0 && ( +
+ {message.generatedImages.map((image, index) => { + const isUploading = uploadingImages[index]; + return ( +
+
+ { + handleImageClick( + image.image_url.url, + index + ); + }} + src={image.image_url.url} + alt={`Generated image ${index + 1}`} + style={{ + width: "100%", + height: "auto", + display: "block", + borderRadius: "8px", + cursor: isUploading ? "wait" : "pointer", + opacity: isUploading ? 0.7 : 1, + transition: "opacity 0.2s ease", + }} + loading="lazy" + /> + + {/* Индикатор загрузки */} + {isUploading && ( +
+ +
+ )} +
+
+ ); + })} +
+ )} + + {/* Блок reasoning если есть */} + {message.reasoning && ( +
+
+ 🧠 Reasoning +
+
+ {message.reasoning} +
+
+ )} + +
+ + {/* Предупреждение о недостаточном балансе */} + {isInsufficientBalance && ( +
+
+ + Недостаточно средств +
+ + Для продолжения работы необходимо пополнить баланс. + Перейдите в профиль, чтобы пополнить счет. + + +
+ )} + + {/* Список цитирований */} + {message.citations && message.citations.length > 0 && ( +
+ {message.citations.map((url, index) => { + const domain = getDomainFromUrl(url); + return ( + + ); + })} +
+ )} + + {message.isAssistant && message.usage && ( +
+ + + {message.usage.promptTokens} In + + + + {message.usage.completionTokens} Out + + + + {message.usage.cost.toFixed(4)} ₽ + +
+ )} + + )} +
+
+
+
+ ); + } +); diff --git a/GPTutor-Frontend-v2/src/panels/Chat/components/MessageList.tsx b/GPTutor-Frontend-v2/src/panels/Chat/components/MessageList.tsx new file mode 100644 index 00000000..b4ed784c --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Chat/components/MessageList.tsx @@ -0,0 +1,68 @@ +import { forwardRef } from "react"; +import { Button, Placeholder } from "@vkontakte/vkui"; +import { Icon28QuestionOutline, Icon28RobotOutline } from "@vkontakte/icons"; +import { MessageListProps } from "../types"; +import { MessageItem } from "./MessageItem"; +import { + EMPTY_CHAT_TITLE, + EMPTY_CHAT_DESCRIPTION, + START_CHAT_BUTTON_TEXT, +} from "../constants"; + +/** + * Список сообщений чата с плейсхолдером для пустого состояния + */ +export const MessageList = forwardRef( + ( + { messages, userViewModel, getUserName, onStartChat, isUploadingFiles }, + ref + ) => { + if (messages.length === 0) { + return ( +
+ } + title={EMPTY_CHAT_TITLE} + > + {EMPTY_CHAT_DESCRIPTION} +
+ +
+
+ ); + } + + return ( +
+ {messages.map((message) => ( + + ))} +
+
+ ); + } +); + +MessageList.displayName = "MessageList"; diff --git a/GPTutor-Frontend-v2/src/panels/Chat/components/index.ts b/GPTutor-Frontend-v2/src/panels/Chat/components/index.ts new file mode 100644 index 00000000..b2b2cc4a --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Chat/components/index.ts @@ -0,0 +1,7 @@ +export { ChatHeader } from "./ChatHeader"; +export { MessageList } from "./MessageList"; +export { MessageItem } from "./MessageItem"; +export { ChatInput } from "./ChatInput"; +export { AttachedFiles } from "./AttachedFiles"; +export { FileUpload } from "./FileUpload"; + diff --git a/GPTutor-Frontend-v2/src/panels/Chat/constants.ts b/GPTutor-Frontend-v2/src/panels/Chat/constants.ts new file mode 100644 index 00000000..306c80d3 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Chat/constants.ts @@ -0,0 +1,30 @@ +/** + * Константы для панели чата + */ + +// Высоты и размеры +export const HEADER_HEIGHT = 60; +export const INPUT_BOTTOM_OFFSET = 110; +export const SCROLL_ARROW_RIGHT_OFFSET = 28; + +// Задержки +export const SCROLL_DELAY_MS = 100; + +// Тексты +export const CHAT_TITLE = "Giga Router"; +export const TYPING_TEXT = "Печатает..."; +export const EMPTY_CHAT_TITLE = "Начните диалог"; +export const EMPTY_CHAT_DESCRIPTION = "Запустите бота"; +export const START_CHAT_BUTTON_TEXT = "Что ты умеешь?"; + +// URL изображений +export const BOT_AVATAR_URL = "https://storage.yandexcloud.net/giga-router/aphex-twin.png"; + +// Z-индексы +export const HEADER_Z_INDEX = 10; +export const SCROLL_ARROW_Z_INDEX = 100; +export const CONTAINER_Z_INDEX = 1; + + + + diff --git a/GPTutor-Frontend-v2/src/panels/Chat/hooks/index.ts b/GPTutor-Frontend-v2/src/panels/Chat/hooks/index.ts new file mode 100644 index 00000000..160f0b60 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Chat/hooks/index.ts @@ -0,0 +1,4 @@ +export { useMessengerScroll } from "./useMessengerScroll"; +export { useChatHandlers } from "./useChatHandlers"; +export { useChatNavigation } from "./useChatNavigation"; + diff --git a/GPTutor-Frontend-v2/src/panels/Chat/hooks/useChatHandlers.ts b/GPTutor-Frontend-v2/src/panels/Chat/hooks/useChatHandlers.ts new file mode 100644 index 00000000..b4924765 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Chat/hooks/useChatHandlers.ts @@ -0,0 +1,57 @@ +import { useCallback } from "react"; +import bridge from "@vkontakte/vk-bridge"; +import { chatViewModel } from "../models"; +import { SCROLL_DELAY_MS } from "../constants"; + +interface UseChatHandlersProps { + scrollToBottom: () => void; + onMessageChange: (message: string) => void; +} + +interface UseChatHandlersReturn { + handleSendMessage: (message: string) => void; + handleStartChat: () => Promise; + handleCopyMessage: (text: string) => Promise; +} + +/** + * Хук для обработки действий в чате + */ +export const useChatHandlers = ({ + scrollToBottom, + onMessageChange, +}: UseChatHandlersProps): UseChatHandlersReturn => { + const handleSendMessage = useCallback( + (message: string) => { + if (!message.trim()) return; + + chatViewModel.sendMessage(message); + onMessageChange(""); + setTimeout(scrollToBottom, SCROLL_DELAY_MS); + }, + [scrollToBottom, onMessageChange] + ); + + const handleStartChat = useCallback(async () => { + await chatViewModel.startWelcomeChat(); + setTimeout(scrollToBottom, SCROLL_DELAY_MS); + }, [scrollToBottom]); + + const handleCopyMessage = useCallback(async (text: string) => { + try { + await bridge.send("VKWebAppCopyText", { text }); + } catch (err) { + console.error("Ошибка копирования:", err); + } + }, []); + + return { + handleSendMessage, + handleStartChat, + handleCopyMessage, + }; +}; + + + + diff --git a/GPTutor-Frontend-v2/src/panels/Chat/hooks/useChatNavigation.ts b/GPTutor-Frontend-v2/src/panels/Chat/hooks/useChatNavigation.ts new file mode 100644 index 00000000..5a003770 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Chat/hooks/useChatNavigation.ts @@ -0,0 +1,36 @@ +import { useCallback } from "react"; +import { useRouteNavigator } from "@vkontakte/vk-mini-apps-router"; + +interface UseChatNavigationReturn { + handleBack: () => void; + handleModelSelect: () => void; +} + +/** + * Хук для навигации в чате + */ +export const useChatNavigation = (): UseChatNavigationReturn => { + const routeNavigator = useRouteNavigator(); + + const handleBack = useCallback(() => { + const viewHistory = (routeNavigator as any).viewHistory?.history; + if (!viewHistory || viewHistory.length <= 1) { + routeNavigator.push("/"); + } else { + routeNavigator.back(); + } + }, [routeNavigator]); + + const handleModelSelect = useCallback(() => { + routeNavigator.push("/models"); + }, [routeNavigator]); + + return { + handleBack, + handleModelSelect, + }; +}; + + + + diff --git a/GPTutor-Frontend-v2/src/panels/Chat/hooks/useMessengerScroll.ts b/GPTutor-Frontend-v2/src/panels/Chat/hooks/useMessengerScroll.ts new file mode 100644 index 00000000..17dfa060 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Chat/hooks/useMessengerScroll.ts @@ -0,0 +1,121 @@ +import { useCallback, useEffect, useRef, useState } from "react"; + +function getScrollBottom(elem: HTMLDivElement) { + return elem.scrollHeight - elem.scrollTop - elem.clientHeight; +} + +const maxAutoScrollValue = 400; +const autoScrollTimeValue = 50; + +export function useMessengerScroll(isTyping: boolean) { + const [showScrollDown, setShowScrollDown] = useState(false); + const scrollRef = useRef(null); + const intervalId = useRef(); + const blockScrollBottom = useRef(false); + const afterTypingTimeout = useRef(null); + + const scrollTimeout = useRef(null); + + const scrollToBottom = useCallback(() => { + if (!scrollRef.current) return; + scrollRef.current.scrollTop = scrollRef.current?.scrollHeight; + }, []); + + useEffect(scrollToBottom, []); + + useEffect(() => { + const handleScrollStart = () => { + blockScrollBottom.current = true; + }; + + const handleScrollEnd = () => { + blockScrollBottom.current = false; + }; + + const currentRef = scrollRef.current; + + currentRef?.addEventListener("touchstart", handleScrollStart); + currentRef?.addEventListener("touchend", handleScrollEnd); + + return () => { + currentRef?.removeEventListener("touchstart", handleScrollStart); + currentRef?.removeEventListener("touchend", handleScrollEnd); + }; + }, []); + + useEffect(() => { + const onScrollDetect = () => { + if (scrollTimeout.current) return; + + scrollTimeout.current = setTimeout(() => { + if (scrollTimeout.current) clearTimeout(scrollTimeout.current); + scrollTimeout.current = null; + }, autoScrollTimeValue); + }; + + const currentRef = scrollRef.current; + currentRef?.addEventListener("scroll", onScrollDetect); + + return () => { + scrollTimeout.current && clearTimeout(scrollTimeout.current); + currentRef?.removeEventListener("scroll", onScrollDetect); + }; + }, []); + + useEffect(() => { + if (!isTyping) { + clearInterval(intervalId.current); + + afterTypingTimeout.current = setTimeout(() => { + scrollToBottom(); + }, 100); + + return () => { + if (afterTypingTimeout.current) { + clearTimeout(afterTypingTimeout.current); + } + }; + } + + intervalId.current = setInterval(() => { + if (blockScrollBottom.current) return; + + if (!scrollRef.current) return; + + if (scrollTimeout.current) return; + + const scrollBottom = getScrollBottom(scrollRef.current); + + if (scrollBottom <= maxAutoScrollValue) { + scrollToBottom(); + } + }, autoScrollTimeValue); + + return () => clearInterval(intervalId.current); + }, [isTyping, scrollToBottom]); + + useEffect(() => { + const handleScroll = () => { + if (!scrollRef.current) return; + + if (getScrollBottom(scrollRef.current) > maxAutoScrollValue) { + setShowScrollDown(true); + return; + } + setShowScrollDown(false); + }; + + const currentRef = scrollRef.current; + if (currentRef) { + currentRef.addEventListener("scroll", handleScroll); + } + + return () => { + if (currentRef) { + currentRef.removeEventListener("scroll", handleScroll); + } + }; + }, []); + + return { scrollRef, scrollToBottom, showScrollDown }; +} diff --git a/GPTutor-Frontend-v2/src/panels/Chat/index.ts b/GPTutor-Frontend-v2/src/panels/Chat/index.ts new file mode 100644 index 00000000..e571b1b2 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Chat/index.ts @@ -0,0 +1,11 @@ +export { Chat } from "./Chat"; +export type { ChatProps } from "./Chat"; +export type { + ChatHeaderProps, + MessageListProps, + MessageItemProps, + ChatInputProps, +} from "./types"; + + + diff --git a/GPTutor-Frontend-v2/src/panels/Chat/models/ChatViewModel.ts b/GPTutor-Frontend-v2/src/panels/Chat/models/ChatViewModel.ts new file mode 100644 index 00000000..046b0a79 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Chat/models/ChatViewModel.ts @@ -0,0 +1,613 @@ +import { makeAutoObservable, runInAction } from "mobx"; +import { MessageModel, MessageRole } from "./MessageModel"; +import { API_BASE_URL, FileInfo, filesApi } from "../../../api"; +import { UploadingFile } from "../types"; +import bridge from "@vkontakte/vk-bridge"; + +/** + * ViewModel для управления состоянием чата + */ +class ChatViewModel { + messages: MessageModel[] = []; + isTyping: boolean = false; + currentModel: string = "openai/gpt-5-nano"; + isOnlineMode: boolean = false; // Режим поиска в сети + isLoading: boolean = false; + error: string | null = null; + attachedFiles: FileInfo[] = []; + uploadingFiles: UploadingFile[] = []; + pendingGeneratedImages: any[] = []; // Сгенерированные изображения для добавления в следующее сообщение пользователя + + private readonly MAX_FILES = 4; + private readonly STORAGE_KEY_MODEL = "chat_current_model"; + + private showSnackbar?: (text: string, subtitle?: string) => void; + + constructor() { + makeAutoObservable(this); + this.loadModelFromStorage(); + } + + /** + * Загрузить модель из VK Storage + */ + private async loadModelFromStorage() { + try { + const result = await bridge.send("VKWebAppStorageGet", { + keys: [this.STORAGE_KEY_MODEL], + }); + + if (result.keys && result.keys.length > 0) { + const savedModel = result.keys[0].value; + if (savedModel) { + runInAction(() => { + this.currentModel = savedModel; + }); + console.log("Loaded model from VK Storage:", savedModel); + } + } + } catch (error) { + console.error("Error loading model from VK Storage:", error); + } + } + + /** + * Сохранить модель в VK Storage + */ + private async saveModelToStorage(model: string) { + try { + await bridge.send("VKWebAppStorageSet", { + key: this.STORAGE_KEY_MODEL, + value: model, + }); + console.log("Saved model to VK Storage:", model); + } catch (error) { + console.error("Error saving model to VK Storage:", error); + } + } + + setSnackbarCallback(callback: (text: string, subtitle?: string) => void) { + this.showSnackbar = callback; + } + + getMessages(): MessageModel[] { + return this.messages; + } + + setModel(model: string) { + console.log(this.currentModel); + this.currentModel = model; + this.saveModelToStorage(model); + } + + /** + * Переключить режим поиска в сети + */ + toggleOnlineMode() { + this.isOnlineMode = !this.isOnlineMode; + } + + /** + * Установить режим поиска в сети + */ + setOnlineMode(isOnline: boolean) { + this.isOnlineMode = isOnline; + } + + /** + * Получить модель с модификатором :online если включен режим + */ + getModelWithModifier(): string { + return this.isOnlineMode + ? `${this.currentModel}:online` + : this.currentModel; + } + + addMessage( + role: MessageRole, + content: string = "", + isTyping: boolean = false, + attachedFiles?: FileInfo[] + ): MessageModel { + const message = new MessageModel( + Date.now().toString(), + role, + content, + isTyping, + attachedFiles + ); + this.messages.push(message); + return message; + } + + async sendMessage(content: string) { + if ( + (!content.trim() && this.attachedFiles.length === 0) || + this.isLoading || + this.uploadingFiles.length > 0 + ) + return; + + // Создаем сообщение пользователя с прикрепленными файлами + const userMessage = this.addMessage( + "user", + content, + false, + this.attachedFiles.length > 0 ? [...this.attachedFiles] : undefined + ); + + // Добавляем сгенерированные изображения к сообщению пользователя + if (this.pendingGeneratedImages.length > 0) { + runInAction(() => { + if (!userMessage.images) { + userMessage.images = []; + } + userMessage.images.push(...this.pendingGeneratedImages); + console.log( + "Added generated images to user message:", + this.pendingGeneratedImages.length + ); + // Очищаем после добавления + this.pendingGeneratedImages = []; + }); + } + + // Очищаем прикрепленные файлы сразу после добавления в сообщение + this.clearAttachedFiles(); + + const assistantMessage = this.addMessage("assistant", "", true); + this.setIsTyping(true); + this.setIsLoading(true); + this.setError(null); + + try { + await this.streamCompletion(assistantMessage); + } catch (error) { + console.error("Error sending message:", error); + + // Проверяем, это ошибка недостаточного баланса + if ( + error instanceof Error && + error.message.includes("Insufficient balance") + ) { + this.setError("insufficient_balance"); + assistantMessage.setContent( + "❌ Недостаточно средств на балансе для отправки сообщения." + ); + } else { + this.setError( + error instanceof Error ? error.message : "Произошла ошибка" + ); + assistantMessage.setContent( + "Извините, произошла ошибка при отправке сообщения." + ); + } + } finally { + assistantMessage.setIsTyping(false); + this.setIsTyping(false); + this.setIsLoading(false); + } + } + + /** + * Получить потоковый ответ от API + */ + private async streamCompletion(message: MessageModel) { + const apiUrl = `${API_BASE_URL}/v1/chat/completions`; + + const formattedMessages = this.messages + .filter((msg) => !msg.isTyping) + .map((msg) => { + if ( + (msg.images && msg.images.length > 0) || + (msg.files && msg.files.length > 0) + ) { + const content: any[] = []; + + if (msg.content) { + content.push({ + type: "text", + text: msg.content, + }); + } + + if (msg.images) { + content.push(...msg.images); + } + + if (msg.files) { + content.push(...msg.files); + } + + return { + role: msg.role, + content: content, + }; + } + + return { + role: msg.role, + content: msg.content, + }; + }); + + const requestBody = { + model: this.getModelWithModifier(), // Используем модель с :online если включен режим + messages: formattedMessages, + stream: true, + temperature: 0.7, + }; + + console.log("Sending request to:", apiUrl); + console.log("Online mode:", this.isOnlineMode); + console.log("Model with modifier:", this.getModelWithModifier()); + console.log("Request body:", JSON.stringify(requestBody, null, 2)); + + const response = await fetch(apiUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${window.location}`, + }, + body: JSON.stringify(requestBody), + }); + + console.log("Response status:", response.status); + console.log("Response headers:", response.headers); + + if (!response.ok) { + // Если статус 402 - недостаточно средств + if (response.status === 402) { + throw new Error("Insufficient balance"); + } + throw new Error(`HTTP error! status: ${response.status}`); + } + + const reader = response.body?.getReader(); + if (!reader) { + throw new Error("No response body"); + } + + const decoder = new TextDecoder(); + let buffer = ""; + + try { + while (true) { + const { done, value } = await reader.read(); + + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; + + for (const line of lines) { + console.log({ line }); + if (line.startsWith("data: ")) { + const data = line.slice(6); + console.log("Received SSE data:", data); + + if (data === "[DONE]") { + console.log("Stream completed"); + return; + } + + try { + const parsed = JSON.parse(data); + console.log(parsed); + const delta = parsed.choices?.[0]?.delta; + const content = delta?.content; + const reasoning = delta?.reasoning; + const annotations = delta?.annotations; + const images = delta?.images; // Сгенерированные изображения + const citations = parsed.citations; + + console.log({ delta }); + + console.log("Parsed content:", content); + console.log("Parsed reasoning:", reasoning); + console.log("Parsed citations:", citations); + console.log("Parsed annotations:", annotations); + console.log("Parsed images:", images); + + if (content !== undefined) { + runInAction(() => { + message.appendContent(content); + }); + } + + if (reasoning) { + runInAction(() => { + message.appendReasoning(reasoning); + }); + } + + if (images && images.length > 0) { + runInAction(() => { + message.addGeneratedImages(images); + this.pendingGeneratedImages.push(...images); + }); + } + + if ( + citations && + Array.isArray(citations) && + citations.length > 0 + ) { + console.log("Setting citations:", citations.length, "URLs"); + runInAction(() => { + message.setCitations(citations); + }); + } + + if ( + annotations && + Array.isArray(annotations) && + annotations.length > 0 + ) { + console.log( + "Setting annotations:", + annotations.length, + "items" + ); + runInAction(() => { + message.setAnnotations(annotations); + }); + } + + if (parsed.usage) { + runInAction(() => { + message.setUsage({ + promptTokens: parsed.usage.prompt_tokens || 0, + completionTokens: parsed.usage.completion_tokens || 0, + totalTokens: parsed.usage.total_tokens || 0, + cost: parsed.usage.cost || 0, + }); + }); + console.log("Usage data:", parsed.usage); + } + + if (parsed.error) { + message.appendContent( + "При выполнении запроса что-то пошло не так! \n Попробуйте позже!" + ); + } + + message.setIsTyping(false); + } catch (parseError) { + console.warn( + "Failed to parse SSE data:", + parseError, + "Data:", + data + ); + } + } + } + } + } finally { + reader.releaseLock(); + } + } + + /** + * Установить состояние печатания + */ + setIsTyping(isTyping: boolean) { + this.isTyping = isTyping; + } + + /** + * Установить состояние загрузки + */ + setIsLoading(isLoading: boolean) { + this.isLoading = isLoading; + } + + /** + * Установить ошибку + */ + setError(error: string | null) { + this.error = error; + } + + /** + * Очистить все сообщения + */ + clearMessages() { + this.messages = []; + this.clearAttachedFiles(); + this.clearUploadingFiles(); + this.pendingGeneratedImages = []; + } + + /** + * Загрузить файл + */ + async uploadFile(file: File) { + // Проверяем лимит файлов + const totalFiles = this.attachedFiles.length + this.uploadingFiles.length; + if (totalFiles >= this.MAX_FILES) { + this.showSnackbar?.( + "Достигнут лимит файлов", + `Можно прикрепить не более ${this.MAX_FILES} файлов` + ); + return Promise.reject(new Error(`Максимум ${this.MAX_FILES} файлов`)); + } + + const isDuplicate = this.attachedFiles.some( + (f) => f.name === file.name && f.size === file.size + ); + + if (isDuplicate) { + this.showSnackbar?.( + "Файл уже прикреплен", + `Файл "${file.name}" уже добавлен` + ); + return Promise.reject(new Error("Файл уже прикреплен")); + } + + // Проверяем, не загружается ли уже файл с таким именем и размером + const isUploading = this.uploadingFiles.some( + (f) => f.file.name === file.name && f.file.size === file.size + ); + + if (isUploading) { + this.showSnackbar?.( + "Файл уже загружается", + `Файл "${file.name}" уже в процессе загрузки` + ); + return Promise.reject(new Error("Файл уже загружается")); + } + + const uploadId = `upload_${Date.now()}_${Math.random() + .toString(36) + .substr(2, 9)}_${file.name.replace(/[^a-zA-Z0-9]/g, "_")}`; + const uploadingFile: UploadingFile = { + id: uploadId, + file: file, + progress: 0, + }; + + // Сразу добавляем файл в список загружающихся + runInAction(() => { + this.uploadingFiles.push(uploadingFile); + console.log( + "Added uploading file:", + uploadingFile.id, + "Total uploading:", + this.uploadingFiles.length + ); + }); + + // Создаем промис для загрузки, но не ждем его здесь + return this.performUpload(uploadingFile); + } + + private async performUpload(uploadingFile: UploadingFile) { + try { + await new Promise((resolve) => setTimeout(resolve, 1500)); + + const response = await filesApi.uploadFile(uploadingFile.file); + + runInAction(() => { + this.uploadingFiles = this.uploadingFiles.filter( + (f) => f.id !== uploadingFile.id + ); + console.log( + "Removed uploading file:", + uploadingFile.id, + "Total uploading:", + this.uploadingFiles.length + ); + + if (response.file) { + this.attachedFiles.push(response.file); + console.log( + "Added attached file:", + response.file.id, + "Total attached:", + this.attachedFiles.length + ); + } + }); + + return response.file; + } catch (error) { + console.error("Error uploading file:", error); + + runInAction(() => { + // Удаляем из загружающихся при ошибке + this.uploadingFiles = this.uploadingFiles.filter( + (f) => f.id !== uploadingFile.id + ); + console.log( + "Removed uploading file due to error:", + uploadingFile.id, + "Total uploading:", + this.uploadingFiles.length + ); + }); + + const errorMessage = + error instanceof Error ? error.message : "Ошибка загрузки файла"; + this.setError(errorMessage); + + // Показываем уведомление об ошибке + this.showSnackbar?.("Ошибка загрузки файла", uploadingFile.file.name); + + throw error; + } + } + + /** + * Удалить прикрепленный файл + */ + removeFile(fileId: string) { + runInAction(() => { + this.attachedFiles = this.attachedFiles.filter( + (file) => file.id !== fileId + ); + }); + } + + /** + * Получить прикрепленные файлы + */ + getAttachedFiles(): FileInfo[] { + return this.attachedFiles; + } + + /** + * Очистить прикрепленные файлы + */ + clearAttachedFiles() { + this.attachedFiles = []; + } + + /** + * Получить загружающиеся файлы + */ + getUploadingFiles(): UploadingFile[] { + return this.uploadingFiles; + } + + /** + * Отменить загрузку файла + */ + cancelUpload(uploadId: string) { + runInAction(() => { + this.uploadingFiles = this.uploadingFiles.filter( + (f) => f.id !== uploadId + ); + }); + } + + /** + * Очистить загружающиеся файлы + */ + clearUploadingFiles() { + this.uploadingFiles = []; + } + + /** + * Начать приветственный диалог + */ + async startWelcomeChat() { + // Блокируем если идет загрузка файлов + if (this.uploadingFiles.length > 0) { + this.showSnackbar?.( + "Дождитесь загрузки файлов", + "Невозможно отправить сообщение во время загрузки файлов" + ); + return; + } + + this.clearMessages(); + await this.sendMessage( + "Привет! Представься и расскажи, чем ты можешь помочь." + ); + } +} + +export const chatViewModel = new ChatViewModel(); diff --git a/GPTutor-Frontend-v2/src/panels/Chat/models/MessageModel.ts b/GPTutor-Frontend-v2/src/panels/Chat/models/MessageModel.ts new file mode 100644 index 00000000..696d1096 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Chat/models/MessageModel.ts @@ -0,0 +1,217 @@ +import { makeAutoObservable } from "mobx"; +import { FileInfo } from "../../../api"; + +/** + * Тип роли сообщения в чате + */ +export type MessageRole = "user" | "assistant" | "system"; + +/** + * Информация об использовании токенов и стоимости запроса + */ +export interface MessageUsage { + promptTokens: number; + completionTokens: number; + totalTokens: number; + cost: number; +} + +/** + * Вложение изображения + */ +export interface ImageAttachment { + type: "image_url"; + image_url: { + url: string; + detail?: "low" | "high" | "auto"; + }; +} + +/** + * Вложение файла + */ +export interface FileAttachment { + type: "file"; + file: { + filename: string; + file_data: string; // URL или base64 + mimeType?: string; + }; +} + +/** + * Цитирование URL + */ +export interface UrlCitation { + type: "url_citation"; + url_citation: { + start_index: number; + end_index: number; + title: string; + url: string; + }; +} + +/** + * Модель сообщения в чате + */ +export class MessageModel { + id: string; + role: MessageRole; + content: string; + reasoning: string = ""; // Рассуждения модели (для моделей с reasoning) + citations: string[] = []; // Список URL цитирований + annotations: UrlCitation[] = []; // Аннотации с деталями цитирований + isTyping: boolean; + timestamp: Date; + usage: MessageUsage | null = null; + images?: ImageAttachment[]; // Изображения от пользователя (входящие) + files?: FileAttachment[]; + attachedFilesList?: FileInfo[]; // Список прикрепленных файлов для отображения + generatedImages: ImageAttachment[] = []; // Изображения сгенерированные моделью (исходящие) + + constructor( + id: string, + role: MessageRole, + content: string = "", + isTyping: boolean = false, + attachedFiles?: FileInfo[] + ) { + this.id = id; + this.role = role; + this.content = content; + this.isTyping = isTyping; + this.timestamp = new Date(); + this.attachedFilesList = attachedFiles; + + // Преобразуем attachedFiles в images и files для API + if (attachedFiles && attachedFiles.length > 0) { + this.images = []; + this.files = []; + + for (const file of attachedFiles) { + if (file.type.startsWith('image/')) { + this.images.push({ + type: "image_url", + image_url: { + url: file.url, + detail: "high" + } + }); + } else { + this.files.push({ + type: "file", + file: { + filename: file.name, + file_data: file.url, + mimeType: file.type + } + }); + } + } + } + + makeAutoObservable(this); + } + + /** + * Установить полное содержимое сообщения + */ + setContent(content: string) { + this.content = content; + } + + /** + * Добавить текст к существующему содержимому (для потоковой передачи) + */ + appendContent(content: string) { + this.content += content; + } + + /** + * Добавить текст к reasoning (для потоковой передачи) + */ + appendReasoning(reasoning: string) { + this.reasoning += reasoning; + } + + /** + * Установить reasoning + */ + setReasoning(reasoning: string) { + this.reasoning = reasoning; + } + + /** + * Установить citations + */ + setCitations(citations: string[]) { + this.citations = citations; + } + + /** + * Установить annotations + */ + setAnnotations(annotations: UrlCitation[]) { + this.annotations = annotations; + } + + /** + * Установить статус печатания + */ + setIsTyping(isTyping: boolean) { + this.isTyping = isTyping; + } + + /** + * Установить информацию об использовании токенов + */ + setUsage(usage: MessageUsage) { + this.usage = usage; + } + + /** + * Добавить сгенерированные изображения (от модели) + */ + addGeneratedImages(images: ImageAttachment[]) { + this.generatedImages.push(...images); + } + + /** + * Установить сгенерированные изображения (от модели) + */ + setGeneratedImages(images: ImageAttachment[]) { + this.generatedImages = images; + } + + /** + * Обновить URL конкретного сгенерированного изображения (для замены base64 на серверный URL) + */ + updateGeneratedImageUrl(imageIndex: number, newUrl: string) { + if (this.generatedImages[imageIndex]) { + this.generatedImages[imageIndex].image_url.url = newUrl; + } + } + + /** + * Проверка, является ли сообщение пользовательским + */ + get isUser() { + return this.role === "user"; + } + + /** + * Проверка, является ли сообщение от ассистента + */ + get isAssistant() { + return this.role === "assistant"; + } + + /** + * Проверка, есть ли сгенерированные изображения + */ + get hasGeneratedImages() { + return this.generatedImages.length > 0; + } +} + diff --git a/GPTutor-Frontend-v2/src/panels/Chat/models/index.ts b/GPTutor-Frontend-v2/src/panels/Chat/models/index.ts new file mode 100644 index 00000000..09796520 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Chat/models/index.ts @@ -0,0 +1,3 @@ +export { MessageModel } from "./MessageModel"; +export { chatViewModel } from "./ChatViewModel"; +export type { MessageRole } from "./MessageModel"; diff --git a/GPTutor-Frontend-v2/src/panels/Chat/styles.ts b/GPTutor-Frontend-v2/src/panels/Chat/styles.ts new file mode 100644 index 00000000..85e80413 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Chat/styles.ts @@ -0,0 +1,66 @@ +import { CSSProperties } from "react"; +import { + HEADER_HEIGHT, + HEADER_Z_INDEX, + CONTAINER_Z_INDEX, + SCROLL_ARROW_Z_INDEX, + INPUT_BOTTOM_OFFSET, + SCROLL_ARROW_RIGHT_OFFSET, +} from "./constants"; + +/** + * Стили для панели чата + */ + +export const chatContainerStyle: CSSProperties = { + display: "flex", + flexDirection: "column", + height: "100vh", + position: "fixed", + top: 0, + left: 0, + right: 0, + bottom: 0, + zIndex: CONTAINER_Z_INDEX, +}; + +export const chatHeaderStyle: CSSProperties = { + position: "fixed", + top: 0, + left: 0, + right: 0, + height: `${HEADER_HEIGHT}px`, + background: "var(--vkui--color_background_contrast_themed)", + borderBottom: "1px solid var(--vkui--color_separator_primary)", + display: "flex", + alignItems: "center", + padding: "0 16px", + zIndex: HEADER_Z_INDEX, +}; + +export const messagesScrollContainerStyle: CSSProperties = { + flexGrow: 1, + overflow: "auto", + scrollBehavior: "smooth", + background: "var(--vkui--color_background_content)", + marginTop: `${HEADER_HEIGHT}px`, +}; + +export const messagesInnerContainerStyle: CSSProperties = { + boxSizing: "border-box", + width: "100%", + minHeight: "100%", + display: "flex", + flexDirection: "column", +}; + +export const scrollArrowContainerStyle: CSSProperties = { + position: "fixed", + bottom: `${INPUT_BOTTOM_OFFSET}px`, + right: `${SCROLL_ARROW_RIGHT_OFFSET}px`, + zIndex: SCROLL_ARROW_Z_INDEX, +}; + + + + diff --git a/GPTutor-Frontend-v2/src/panels/Chat/types.ts b/GPTutor-Frontend-v2/src/panels/Chat/types.ts new file mode 100644 index 00000000..2c36b90d --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Chat/types.ts @@ -0,0 +1,49 @@ +import { MessageModel } from "./models"; +import { UserViewModel } from "../../viewModels/UserViewModel"; +import { FileInfo } from "../../api/filesApi"; + +/** + * Типы для компонентов чата + */ + +export interface ChatHeaderProps { + isTyping: boolean; + onBack: () => void; +} + +export interface MessageListProps { + messages: MessageModel[]; + userViewModel: UserViewModel; + getUserName: () => string; + onStartChat: () => void; + isUploadingFiles?: boolean; +} + +export interface MessageItemProps { + message: MessageModel; + userViewModel: UserViewModel; + getUserName: () => string; +} + +export interface UploadingFile { + id: string; + file: File; + progress?: number; +} + +export interface ChatInputProps { + message: string; + onMessageChange: (message: string) => void; + onSendMessage: () => void; + disabled?: boolean; + currentModel: string; + isOnlineMode?: boolean; + onModelSelect: () => void; + onOnlineModeToggle?: () => void; + onClearMessages: () => void; + attachedFiles?: FileInfo[]; + uploadingFiles?: UploadingFile[]; + onFileUpload?: (file: File) => void; + onFileRemove?: (fileId: string) => void; + onCancelUpload?: (uploadId: string) => void; +} diff --git a/GPTutor-Frontend-v2/src/panels/Home/HeroSection.tsx b/GPTutor-Frontend-v2/src/panels/Home/HeroSection.tsx new file mode 100644 index 00000000..e3ae6acd --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Home/HeroSection.tsx @@ -0,0 +1,57 @@ +import { FC } from "react"; +import { DisplayTitle, Div, Group, Headline, Spacing } from "@vkontakte/vkui"; + +// const DEFAULT_MESSAGE = "Что ты умеешь?"; + +export const HeroSection: FC = () => { + // const [message, setMessage] = useState(""); + // const routeNavigator = useRouteNavigator(); + + // const handleSendMessage = () => { + // const textToSend = message.trim() || DEFAULT_MESSAGE; + // + // // Отправляем сообщение + // chatViewModel.sendMessage(textToSend); + // + // // Переходим в чат + // routeNavigator.push("/chat"); + // }; + + // const handleKeyDown = (e: React.KeyboardEvent) => { + // if (e.key === "Enter" && !e.shiftKey) { + // e.preventDefault(); + // handleSendMessage(); + // } + // }; + + return ( + +
+ + Единый API интерфейс нейросетей
в одном сервисе! +
+ + + Без ВПН и зарубежных карт! + + {/**/} + {/* setMessage(e.target.value)}*/} + {/* onKeyDown={handleKeyDown}*/} + {/* after={*/} + {/* */} + {/* }*/} + {/* placeholder={DEFAULT_MESSAGE}*/} + {/*/>*/} +
+
+ ); +}; diff --git a/GPTutor-Frontend-v2/src/panels/Home/Home.module.css b/GPTutor-Frontend-v2/src/panels/Home/Home.module.css new file mode 100644 index 00000000..3f26bdf6 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Home/Home.module.css @@ -0,0 +1,6 @@ +.stepsSectionWrapper :global(.vkuiHorizontalScroll__in) { + padding-top: var(--vkui--size_base_padding_vertical--regular); + padding-bottom: var(--vkui--size_base_padding_vertical--regular); + margin-left: calc(var(--vkui--size_base_padding_vertical--regular)); + margin-right: calc(var(--vkui--size_base_padding_vertical--regular)); +} diff --git a/GPTutor-Frontend-v2/src/panels/Home/Home.tsx b/GPTutor-Frontend-v2/src/panels/Home/Home.tsx new file mode 100644 index 00000000..4bd3b76a --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Home/Home.tsx @@ -0,0 +1,20 @@ +import { FC } from "react"; +import { NavIdProps, Panel, PanelHeader } from "@vkontakte/vkui"; +import { HeroSection } from "./HeroSection"; +import { StepsSection } from "./StepsSection"; +import { ModelsCard } from "./ModelsCard"; + +export interface HomeProps extends NavIdProps {} + +export const Home: FC = ({ id }) => { + return ( + + LLM API + + + + + ); +}; + + diff --git a/GPTutor-Frontend-v2/src/panels/Home/ModelsCard.tsx b/GPTutor-Frontend-v2/src/panels/Home/ModelsCard.tsx new file mode 100644 index 00000000..6bcef6f6 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Home/ModelsCard.tsx @@ -0,0 +1,55 @@ +import { FC } from "react"; +import { + ContentCard, + DisplayTitle, + Div, + Group, + Headline, + Link, + useAdaptivityWithJSMediaQueries, +} from "@vkontakte/vkui"; +import { useRouteNavigator } from "@vkontakte/vk-mini-apps-router"; +import { Icon16LinkOutline } from "@vkontakte/icons"; +import { DEFAULT_VIEW_PANELS } from "../../routes"; + +export const ModelsCard: FC = () => { + const navigator = useRouteNavigator(); + const { isDesktop } = useAdaptivityWithJSMediaQueries(); + + const handleModelsClick = () => { + navigator.push(`/${DEFAULT_VIEW_PANELS.MODELS}`); + }; + + return ( + +
+ Одно API для всех моделей!} + description={ + + Доступ ко всем основным моделям осуществляется через единый + интерфейс. OpenAI SDK работает "из коробки". + + } + caption={ + }> + Посмотреть модели + + } + maxHeight={200} + /> +
+
+ ); +}; + diff --git a/GPTutor-Frontend-v2/src/panels/Home/StepCard.tsx b/GPTutor-Frontend-v2/src/panels/Home/StepCard.tsx new file mode 100644 index 00000000..2e9e96df --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Home/StepCard.tsx @@ -0,0 +1,66 @@ +import { FC, ReactNode } from "react"; +import { + Button, + Card, + Counter, + DisplayTitle, + Div, + Flex, + Spacing, +} from "@vkontakte/vkui"; + +interface StepCardProps { + stepNumber: string; + title: string; + icon: ReactNode; + description: ReactNode; + buttonText: string; + buttonIcon: ReactNode; + onButtonClick: () => void; +} + +export const StepCard: FC = ({ + stepNumber, + title, + icon, + description, + buttonText, + buttonIcon, + onButtonClick, +}) => { + return ( + +
+ + + {stepNumber} + + {title} + + + + {icon} + + {description} + + + +
+
+ ); +}; diff --git a/GPTutor-Frontend-v2/src/panels/Home/StepsSection.tsx b/GPTutor-Frontend-v2/src/panels/Home/StepsSection.tsx new file mode 100644 index 00000000..ee159505 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Home/StepsSection.tsx @@ -0,0 +1,252 @@ +import { FC } from "react"; +import { + CardScroll, + DisplayTitle, + Flex, + Group, + useAdaptivityWithJSMediaQueries, +} from "@vkontakte/vkui"; +import { useRouteNavigator } from "@vkontakte/vk-mini-apps-router"; +import { + Icon24BracketsSlashOutline, + Icon24DocumentTextOutline, + Icon28KeyOutline, + Icon28KeySquareOutline, + Icon28MoneySendOutline, + Icon28PaymentCardAddOutline, +} from "@vkontakte/icons"; +import { DEFAULT_VIEW_PANELS } from "../../routes"; +import { StepCard } from "./StepCard"; + +import classes from "./Home.module.css"; + +export const StepsSection: FC = () => { + const navigator = useRouteNavigator(); + const { isDesktop } = useAdaptivityWithJSMediaQueries(); + + const handleProfileClick = () => { + navigator.push(`/${DEFAULT_VIEW_PANELS.PROFILE}`); + }; + + const handleDocsClick = () => { + window.open("https://docs.giga-router.ru/", "_blank"); + }; + + if (!isDesktop) { + return ( + + + + } + description={ + sk - •••••••••••••••••• + } + buttonText="Получить ключ" + buttonIcon={} + onButtonClick={handleProfileClick} + /> + + + } + description={ + +
+ +
+
+ +
+
+ +
+
+ } + buttonText="Документация" + buttonIcon={} + onButtonClick={handleDocsClick} + /> +
+
+ ); + } + + return ( + + + } + description={ + + 50₽ Бесплатно! + + } + buttonText="Посмотреть баланс" + buttonIcon={} + onButtonClick={handleProfileClick} + /> + + + } + description={ + sk - •••••••••••••••••• + } + buttonText="Получить ключ" + buttonIcon={} + onButtonClick={handleProfileClick} + /> + + + } + description={ + +
+ +
+
+ +
+
+ +
+
+ } + buttonText="Документация" + buttonIcon={} + onButtonClick={handleDocsClick} + /> +
+
+ ); +}; diff --git a/GPTutor-Frontend-v2/src/panels/Home/index.ts b/GPTutor-Frontend-v2/src/panels/Home/index.ts new file mode 100644 index 00000000..38c27132 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Home/index.ts @@ -0,0 +1,7 @@ +export { Home } from "./Home.tsx"; +export { HeroSection } from "./HeroSection.tsx"; +export { StepCard } from "./StepCard.tsx"; +export { StepsSection } from "./StepsSection.tsx"; +export { ModelsCard } from "./ModelsCard.tsx"; + +export type { HomeProps } from "./Home.tsx"; diff --git a/GPTutor-Frontend-v2/src/panels/Models/ModelCard.tsx b/GPTutor-Frontend-v2/src/panels/Models/ModelCard.tsx new file mode 100644 index 00000000..f514d7c6 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Models/ModelCard.tsx @@ -0,0 +1,141 @@ +import { FC } from "react"; +import { + Button, + Card, + EllipsisText, + Flex, + Spacing, + Text, + Title, + useAdaptivityWithJSMediaQueries, +} from "@vkontakte/vkui"; +import { Icon16Message } from "@vkontakte/icons"; +import { + formatContextLength, + formatModalities, + ProcessedModel, +} from "../../api"; +import { ModelIconService } from "../../services/ModelIconService"; +import { CopyButton } from "../../components"; + +interface ModelCardProps { + model: ProcessedModel; + onCopyModelId: (modelId: string) => void; + onTryModel: (modelId: string) => void; +} + +export const ModelCard: FC = ({ + model, + onCopyModelId, + onTryModel, +}) => { + const { isDesktop } = useAdaptivityWithJSMediaQueries(); + + return ( + + + + + +
+ {ModelIconService.getModelIcon(model.name)} +
+ + + <Spacing size={2} /> + + <EllipsisText maxWidth={isDesktop ? 500 : 200}> + {model.name} + </EllipsisText> + + onCopyModelId(model.id)} + /> + +
+ + + {formatContextLength(model.contextLength)} •{" "} + {formatModalities(model.inputModalities)} + +
+
+ + {/* Price and Action Section */} + + + {isDesktop ? ( + + + {model.price} + + {model.price !== "Бесплатно" && ( + + за 1М токенов + + )} + + ) : ( +
+ )} + + +
+
+
+ ); +}; diff --git a/GPTutor-Frontend-v2/src/panels/Models/Models.tsx b/GPTutor-Frontend-v2/src/panels/Models/Models.tsx new file mode 100644 index 00000000..524139cf --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Models/Models.tsx @@ -0,0 +1,113 @@ +import { FC, useEffect } from "react"; +import { + Button, + Div, + Group, + NavIdProps, + Panel, + PanelHeader, + PanelHeaderBack, + Spacing, + Spinner, + Text, + Title, +} from "@vkontakte/vkui"; +import { useRouteNavigator } from "@vkontakte/vk-mini-apps-router"; +import { useModelsViewModel } from "../../viewModels/ModelsViewModel"; +import { SearchSection } from "./SearchSection"; +import { QuickSearchSection } from "./QuickSearchSection"; +import { ModelsList } from "./ModelsList"; +import { Icon28LinkOutline } from "@vkontakte/icons"; + +export interface ModelsProps extends NavIdProps {} + +export const Models: FC = ({ id }) => { + const navigator = useRouteNavigator(); + const { + filteredModels, + searchQuery, + sortOrder, + loading, + handleSearchChange, + handleQuickSearch, + handleSortToggle, + copyModelId, + tryModel, + loadModels, + } = useModelsViewModel(); + + const handleTryModel = (modelId: string) => { + tryModel(modelId); + navigator.push("/chat"); + }; + + useEffect(() => { + loadModels(); + }, [loadModels]); + + return ( + + navigator.back()} />} + > + Модели + + + {loading ? ( + +
+ +
+
+ ) : ( + <> + +
+ Доступные модели + + + Выберите подходящую модель для ваших задач. Все модели работают + через единый API. + + + +
+
+ + + + + + + + + + )} +
+ ); +}; diff --git a/GPTutor-Frontend-v2/src/panels/Models/ModelsList.tsx b/GPTutor-Frontend-v2/src/panels/Models/ModelsList.tsx new file mode 100644 index 00000000..2b1c73f0 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Models/ModelsList.tsx @@ -0,0 +1,31 @@ +import { FC } from "react"; +import { Div, Spacing } from "@vkontakte/vkui"; +import { ProcessedModel } from "../../api"; +import { ModelCard } from "./ModelCard"; + +interface ModelsListProps { + models: ProcessedModel[]; + onCopyModelId: (modelId: string) => void; + onTryModel: (modelId: string) => void; +} + +export const ModelsList: FC = ({ + models, + onCopyModelId, + onTryModel, +}) => { + return ( +
+ {models.map((model, index) => ( +
+ + {index < models.length - 1 && } +
+ ))} +
+ ); +}; \ No newline at end of file diff --git a/GPTutor-Frontend-v2/src/panels/Models/QuickSearchSection.tsx b/GPTutor-Frontend-v2/src/panels/Models/QuickSearchSection.tsx new file mode 100644 index 00000000..f2508bf4 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Models/QuickSearchSection.tsx @@ -0,0 +1,63 @@ +import { FC } from "react"; +import { + Caption, + SubnavigationBar, + SubnavigationButton, +} from "@vkontakte/vkui"; +import { ModelIconService } from "../../services/ModelIconService"; + +interface QuickSearchSectionProps { + onQuickSearch: (query: string) => void; +} + +interface QuickSearchButton { + query: string; + label: string; + modelName: string; +} + +export const QuickSearchSection: FC = ({ + onQuickSearch, +}) => { + const quickSearchButtons: QuickSearchButton[] = [ + { query: "gpt", label: "GPT", modelName: "gpt" }, + { query: "deepseek", label: "DeepSeek", modelName: "deepseek" }, + { query: "grok", label: "Grok", modelName: "grok" }, + { query: "gemini", label: "Gemini", modelName: "gemini" }, + { query: "mistral", label: "Mistral", modelName: "mistral" }, + { query: "qwen", label: "Qwen", modelName: "qwen" }, + { query: "perplexity", label: "Perplexity", modelName: "perplexity" }, + { query: "claude", label: "Claude", modelName: "anthropic" }, + ]; + + return ( + + onQuickSearch("")} + > + + Все модели + + + {quickSearchButtons.map((button) => ( + onQuickSearch(button.query)} + before={ +
+ {ModelIconService.getModelIconSmall(button.modelName)} +
+ } + > + {button.label} +
+ ))} +
+ ); +}; diff --git a/GPTutor-Frontend-v2/src/panels/Models/SearchSection.tsx b/GPTutor-Frontend-v2/src/panels/Models/SearchSection.tsx new file mode 100644 index 00000000..2d24ac90 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Models/SearchSection.tsx @@ -0,0 +1,77 @@ +import { FC } from "react"; +import { Button, Card, Div, Flex, Search, Text } from "@vkontakte/vkui"; +import { + Icon12ArrowDown, + Icon12ArrowUp, + Icon20SortOutline, +} from "@vkontakte/icons"; + +interface SearchSectionProps { + searchQuery: string; + sortOrder: "asc" | "desc"; + onSearchChange: (event: React.ChangeEvent) => void; + onSortToggle: () => void; +} + +export const SearchSection: FC = ({ + searchQuery, + sortOrder, + onSearchChange, + onSortToggle, +}) => { + const getSortIcon = () => { + if (sortOrder === "asc") { + return ; + } + return ; + }; + + const getSortText = () => { + return sortOrder === "asc" ? "Дешевые сначала" : "Дорогие сначала"; + }; + + return ( + <> + +
+ + + + + + Цена + + + + + + +
+ + ); +}; diff --git a/GPTutor-Frontend-v2/src/panels/Models/index.ts b/GPTutor-Frontend-v2/src/panels/Models/index.ts new file mode 100644 index 00000000..5ac7d0b7 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Models/index.ts @@ -0,0 +1,7 @@ +export { Models } from "./Models.tsx"; +export { SearchSection } from "./SearchSection.tsx"; +export { QuickSearchSection } from "./QuickSearchSection.tsx"; +export { ModelCard } from "./ModelCard.tsx"; +export { ModelsList } from "./ModelsList.tsx"; + +export type { ModelsProps } from "./Models.tsx"; \ No newline at end of file diff --git a/GPTutor-Frontend-v2/src/panels/Persik.tsx b/GPTutor-Frontend-v2/src/panels/Persik.tsx new file mode 100644 index 00000000..5fb6a7f0 --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Persik.tsx @@ -0,0 +1,19 @@ +import { FC } from 'react'; +import { NavIdProps, Panel, PanelHeader, PanelHeaderBack, Placeholder } from '@vkontakte/vkui'; +import { useRouteNavigator } from '@vkontakte/vk-mini-apps-router'; +import PersikImage from '../assets/persik.png'; + +export const Persik: FC = ({ id }) => { + const routeNavigator = useRouteNavigator(); + + return ( + + routeNavigator.back()} />}> + Persik + + + Persik The Cat + + + ); +}; diff --git a/GPTutor-Frontend-v2/src/panels/Profile/ApiKeySection.tsx b/GPTutor-Frontend-v2/src/panels/Profile/ApiKeySection.tsx new file mode 100644 index 00000000..c990ec1a --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Profile/ApiKeySection.tsx @@ -0,0 +1,53 @@ +import { FC } from "react"; +import { Button, Div, Flex, Group, Spacing, Title } from "@vkontakte/vkui"; +import { Icon28RefreshOutline } from "@vkontakte/icons"; +import { createCodeHTML } from "../../utils/codeFormatter"; +import { CopyButton } from "../../components"; + +interface ApiKeySectionProps { + apiKey: string; + updatingToken: boolean; + onUpdateToken: () => void; +} + +export const ApiKeySection: FC = ({ + apiKey, + updatingToken, + onUpdateToken, +}) => { + return ( + +
+ API Ключ + + +
+ + + + +
+ + ); +}; diff --git a/GPTutor-Frontend-v2/src/panels/Profile/BalanceSection.tsx b/GPTutor-Frontend-v2/src/panels/Profile/BalanceSection.tsx new file mode 100644 index 00000000..7fd60acc --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Profile/BalanceSection.tsx @@ -0,0 +1,139 @@ +import { FC } from "react"; +import { + Button, + Div, + Flex, + Group, + IconButton, + Link, + Spacing, + Title, + useAdaptivityWithJSMediaQueries, +} from "@vkontakte/vkui"; +import { Icon28MoneySendOutline, Icon24RefreshOutline } from "@vkontakte/icons"; +import { useRouteNavigator } from "@vkontakte/vk-mini-apps-router"; +import { createCodeHTML } from "../../utils/codeFormatter"; +import { userViewModel } from "../../viewModels/UserViewModel"; +import { CopyButton } from "../../components"; +import { MODALS } from "../../routes"; +import { observer } from "mobx-react-lite"; + +interface BalanceSectionProps { + balance: number; + onReload?: () => void; +} + +export const BalanceSection: FC = observer( + ({ balance, onReload }) => { + const routeNavigator = useRouteNavigator(); + const { isDesktop } = useAdaptivityWithJSMediaQueries(); + + const handleTopUp = () => { + routeNavigator.showModal(MODALS.TOP_UP_BALANCE); + }; + + return ( + +
+ Профиль + + +
+ + + + {isDesktop && ( + <> + + +
+ +
+ {balance.toFixed(4)}₽ +
+ Доступно для использования +
+
+ {onReload && ( + + + + )} +
+ +
+ +
+ +
+
+
+ + Пользовательское соглашение + + , +
+ + Политика конфиденциальности + +
+ + ИНН:{" "} + + 027701131663 + + +
+ + )} +
+ + ); + } +); diff --git a/GPTutor-Frontend-v2/src/panels/Profile/CodeExamplesSection.tsx b/GPTutor-Frontend-v2/src/panels/Profile/CodeExamplesSection.tsx new file mode 100644 index 00000000..d1426acb --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Profile/CodeExamplesSection.tsx @@ -0,0 +1,112 @@ +import { FC } from "react"; +import { + Button, + Div, + Flex, + Group, + SegmentedControl, + Spacing, + Title, + useAdaptivityWithJSMediaQueries, +} from "@vkontakte/vkui"; +import { Icon24BrainOutline, Icon28LinkOutline } from "@vkontakte/icons"; + +import { + CodeExampleService, + CodeExampleType, +} from "../../services/CodeExampleService"; +import { getCodeStyles } from "../../utils/codeFormatter"; +import { CopyButton } from "../../components"; +import { useRouteNavigator } from "@vkontakte/vk-mini-apps-router"; +import { DEFAULT_VIEW_PANELS } from "../../routes.ts"; + +interface CodeExamplesSectionProps { + apiKey: string; + activeCodeExample: CodeExampleType; + onCodeExampleChange: (type: CodeExampleType) => void; +} + +export const CodeExamplesSection: FC = ({ + apiKey, + activeCodeExample, + onCodeExampleChange, +}) => { + const codeHTML = CodeExampleService.getCodeHTML(activeCodeExample, apiKey); + const rawCode = CodeExampleService.getRawCode(activeCodeExample, apiKey); + const { isDesktop } = useAdaptivityWithJSMediaQueries(); + const navigator = useRouteNavigator(); + + const handleModelsClick = () => { + navigator.push(`/${DEFAULT_VIEW_PANELS.MODELS}`); + }; + + return ( + +
+ Примеры использования + + onCodeExampleChange(value as CodeExampleType)} + options={[ + { + label: "Curl", + value: "curl", + }, + { + label: "Python", + value: "python", + }, + { + label: "JavaScript", + value: "js", + }, + ]} + /> + + +
+ + + + + + + + + + + + +
+ + ); +}; diff --git a/GPTutor-Frontend-v2/src/panels/Profile/Profile.tsx b/GPTutor-Frontend-v2/src/panels/Profile/Profile.tsx new file mode 100644 index 00000000..f23bb33b --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Profile/Profile.tsx @@ -0,0 +1,73 @@ +import { FC, useEffect } from "react"; +import { + NavIdProps, + Panel, + PanelHeader, + Placeholder, + ScreenSpinner, +} from "@vkontakte/vkui"; +import { useProfileViewModel } from "../../viewModels/ProfileViewModel"; +import { BalanceSection } from "./BalanceSection"; +import { ApiKeySection } from "./ApiKeySection"; +import { CodeExamplesSection } from "./CodeExamplesSection"; +import "../../styles/prism.css"; +import { observer } from "mobx-react-lite"; + +export interface ProfileProps extends NavIdProps {} + +export const Profile: FC = observer(({ id }) => { + const { + profile, + vkData, + loading, + updatingToken, + activeCodeExample, + setActiveCodeExample, + loadProfile, + updateToken, + } = useProfileViewModel(); + + useEffect(() => { + loadProfile(); + }, [loadProfile]); + + if (loading) { + return ( + + Профиль + + + + + ); + } + + if (!profile || !vkData) { + return ( + + Профиль + Не удалось загрузить данные профиля + + ); + } + + return ( + + Профиль + + + + + + + + ); +}); diff --git a/GPTutor-Frontend-v2/src/panels/Profile/index.ts b/GPTutor-Frontend-v2/src/panels/Profile/index.ts new file mode 100644 index 00000000..23f9e54f --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/Profile/index.ts @@ -0,0 +1,8 @@ +export { Profile } from "./Profile.tsx"; +export { BalanceSection } from "./BalanceSection.tsx"; +export { ApiKeySection } from "./ApiKeySection.tsx"; +export { CodeExamplesSection } from "./CodeExamplesSection.tsx"; + +export type { ProfileProps } from "./Profile.tsx"; + + diff --git a/GPTutor-Frontend-v2/src/panels/index.ts b/GPTutor-Frontend-v2/src/panels/index.ts new file mode 100644 index 00000000..5cfe8eff --- /dev/null +++ b/GPTutor-Frontend-v2/src/panels/index.ts @@ -0,0 +1,10 @@ +export { Persik } from "./Persik.tsx"; +export { Home } from "./Home"; +export { Models } from "./Models"; +export { Profile } from "./Profile"; +export { Chat } from "./Chat"; + +export type { HomeProps } from "./Home"; +export type { ModelsProps } from "./Models"; +export type { ProfileProps } from "./Profile"; +export type { ChatProps } from "./Chat"; diff --git a/GPTutor-Frontend-v2/src/routes.ts b/GPTutor-Frontend-v2/src/routes.ts new file mode 100644 index 00000000..4dbe1973 --- /dev/null +++ b/GPTutor-Frontend-v2/src/routes.ts @@ -0,0 +1,44 @@ +import { + createHashRouter, + createModal, + createPanel, + createRoot, + createView, + RoutesConfig, +} from '@vkontakte/vk-mini-apps-router'; + +export const DEFAULT_ROOT = 'default_root'; + +export const DEFAULT_VIEW = 'default_view'; + +export const DEFAULT_VIEW_PANELS = { + HOME: 'home', + PERSIK: 'persik', + PROFILE: 'profile', + CHAT: 'chat', + MODELS: 'models', +} as const; + +export const MODALS = { + TOP_UP_BALANCE: 'top-up-balance', +} as const; + +export const routes = RoutesConfig.create([ + createRoot(DEFAULT_ROOT, [ + createView('home', [ + createPanel(DEFAULT_VIEW_PANELS.HOME, '/', []), + createPanel(DEFAULT_VIEW_PANELS.PERSIK, `/${DEFAULT_VIEW_PANELS.PERSIK}`, []), + createPanel(DEFAULT_VIEW_PANELS.MODELS, `/${DEFAULT_VIEW_PANELS.MODELS}`, []), + ]), + createView('profile', [ + createPanel(DEFAULT_VIEW_PANELS.PROFILE, `/${DEFAULT_VIEW_PANELS.PROFILE}`, [ + createModal(MODALS.TOP_UP_BALANCE, MODALS.TOP_UP_BALANCE), + ]), + ]), + createView('chat', [ + createPanel(DEFAULT_VIEW_PANELS.CHAT, `/${DEFAULT_VIEW_PANELS.CHAT}`, []), + ]), + ]), +]); + +export const router = createHashRouter(routes.getRoutes()); diff --git a/GPTutor-Frontend-v2/src/services/CodeExampleService.ts b/GPTutor-Frontend-v2/src/services/CodeExampleService.ts new file mode 100644 index 00000000..76daf9b0 --- /dev/null +++ b/GPTutor-Frontend-v2/src/services/CodeExampleService.ts @@ -0,0 +1,97 @@ +import { createCodeHTML } from "../utils/codeFormatter"; + +export type CodeExampleType = "curl" | "python" | "js"; + +export interface CodeExample { + code: string; + language: string; + html: string; +} + +export class CodeExampleService { + private static getBaseUrl(): string { + return import.meta.env.VITE_API_URL || "http://localhost:3000/api"; + } + + static generateCodeExample( + type: CodeExampleType, + apiKey: string + ): CodeExample { + const baseUrl = this.getBaseUrl(); + let code = ""; + let language = ""; + + switch (type) { + case "curl": + code = `curl -X POST "${baseUrl}/v1/chat/completions" \\ + -H "Content-Type: application/json" \\ + -H "Authorization: Bearer ${apiKey}" \\ + -d '{ + "model": "google/gemini-2.5-flash-lite", + "messages": [ + {"role": "user", "content": "Привет!"} + ] + }'`; + language = "bash"; + break; + + case "python": + code = `import requests + +url = "${baseUrl}/v1/chat/completions" +headers = { + "Content-Type": "application/json", + "Authorization": "Bearer ${apiKey}" +} +data = { + "model": "google/gemini-2.5-flash-lite", + "messages": [ + {"role": "user", "content": "Привет!"} + ] +} + +response = requests.post(url, json=data, headers=headers) +print(response.json())`; + language = "python"; + break; + + case "js": + code = `const response = await fetch('${baseUrl}/v1/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ${apiKey}' + }, + body: JSON.stringify({ + model: 'google/gemini-2.5-flash-lite', + messages: [ + { role: 'user', content: 'Привет!' } + ] + }) +}); + +const data = await response.json(); +console.log(data);`; + language = "javascript"; + break; + + default: + throw new Error(`Unsupported code example type: ${type}`); + } + + return { + code, + language, + html: createCodeHTML(code, language), + }; + } + + static getRawCode(type: CodeExampleType, apiKey: string): string { + return this.generateCodeExample(type, apiKey).code; + } + + static getCodeHTML(type: CodeExampleType, apiKey: string): string { + return this.generateCodeExample(type, apiKey).html; + } +} + diff --git a/GPTutor-Frontend-v2/src/services/Markdown.tsx b/GPTutor-Frontend-v2/src/services/Markdown.tsx new file mode 100644 index 00000000..de86ae5c --- /dev/null +++ b/GPTutor-Frontend-v2/src/services/Markdown.tsx @@ -0,0 +1,100 @@ +/* eslint-disable */ + +import MarkdownIt from "markdown-it"; +import Prism from "prismjs"; + +//@ts-ignore +import mila from "markdown-it-link-attributes"; + +// Импортируем компоненты Prism статически +import 'prismjs/components/prism-go.min.js'; +import 'prismjs/components/prism-python.min.js'; +import 'prismjs/components/prism-bash.min.js'; +import 'prismjs/components/prism-javascript.min.js'; +import 'prismjs/components/prism-json.min.js'; +import 'prismjs/components/prism-markup.min.js'; + +const getLanguage = (lang: string) => { + return !lang || lang === "html" ? "markup" : lang; +}; + +export default class Markdown { + markdownItWithPlugins = new MarkdownIt({ + breaks: true, + langPrefix: "language-", + linkify: true, + typographer: false, + highlight: function (code: string, lang: string) { + const language = getLanguage(lang); + const grammar = Prism.languages[language]; + + try { + if (grammar) { + return Prism.highlight(code, grammar, language); + } + } catch (err) { + console.error(err); + } + + return code; + }, + }) + .use(mila, { attrs: { target: "_blank" } }); + + markdownIt = new MarkdownIt({ + breaks: true, + langPrefix: "language-", + linkify: true, + typographer: false, + highlight: function (code: string, lang: string) { + const language = getLanguage(lang); + const grammar = Prism.languages[language]; + + try { + if (grammar) { + return Prism.highlight(code, grammar, language); + } + } catch (err) { + console.error(err); + } + + return code; + }, + }); + + markdownItHtml = new MarkdownIt({ + breaks: true, + html: true, + langPrefix: "language-", + linkify: true, + typographer: false, + highlight: function (code: string, lang: string) { + const language = getLanguage(lang); + const grammar = Prism.languages[language]; + + try { + if (grammar) { + return Prism.highlight(code, grammar, language); + } + } catch (err) { + console.error(err); + } + + return code; + }, + }) + .use(mila, { attrs: { target: "_blank" } }); + + render(markdown: string) { + return this.markdownItWithPlugins.render(markdown); + } + + renderWithoutPlugins(markdown: string) { + return this.markdownIt.render(markdown); + } + + renderHtml(markdown: string) { + return this.markdownItHtml.render(markdown); + } + +} diff --git a/GPTutor-Frontend-v2/src/services/ModelIconService.tsx b/GPTutor-Frontend-v2/src/services/ModelIconService.tsx new file mode 100644 index 00000000..22f1de69 --- /dev/null +++ b/GPTutor-Frontend-v2/src/services/ModelIconService.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import { + GeminiIcon, + QwenIcon, + DeepSeekIcon, + GrokIcon, + OpenAIIcon, + MistralIcon, + ClaudeIcon, + PerplexityIcon, +} from "../components/icons"; + +export class ModelIconService { + static getModelIconSmall(modelName: string): React.ReactNode { + const name = modelName.toLowerCase(); + + if (name.includes("gemini")) { + return ; + } else if (name.includes("qwen")) { + return ; + } else if (name.includes("deepseek")) { + return ; + } else if (name.includes("grok")) { + return ; + } else if (name.includes("gpt") || name.includes("openai")) { + return ; + } else if (name.includes("mistral")) { + return ; + } else if (name.includes("perplexity")) { + return ; + } else if (name.includes("anthropic")) { + return ; + } + + return null; + } + + static getModelIcon(modelName: string): React.ReactNode { + const name = modelName.toLowerCase(); + + if (name.includes("google")) { + return ; + } else if (name.includes("qwen")) { + return ; + } else if (name.includes("deepseek")) { + return ; + } else if (name.includes("grok")) { + return ; + } else if (name.includes("gpt") || name.includes("openai")) { + return ; + } else if (name.includes("mistral")) { + return ; + } else if (name.includes("anthropic")) { + return ; + } else if (name.includes("perplexity")) { + return ; + } + + return null; + } + + static getIconContainerStyle() { + return { + width: "20px", + height: "20px", + display: "flex", + alignItems: "center", + justifyContent: "center", + borderRadius: "50%", + background: "#f0f2f5", + padding: "2px", + }; + } +} diff --git a/GPTutor-Frontend-v2/src/services/ModelsService.tsx b/GPTutor-Frontend-v2/src/services/ModelsService.tsx new file mode 100644 index 00000000..b3b3eb3e --- /dev/null +++ b/GPTutor-Frontend-v2/src/services/ModelsService.tsx @@ -0,0 +1,145 @@ +import React from "react"; +import { Snackbar, Button } from "@vkontakte/vkui"; +import { Icon12Check, Icon12Cancel } from "@vkontakte/icons"; +import { modelsApi, ProcessedModel, processModelData } from "../api"; + +export class ModelsService { + static async fetchModels(): Promise { + try { + const data = await modelsApi.getModels(); + + if (data.success && data.data.models) { + return data.data.models.map(processModelData); + } else { + throw new Error("Неожиданная структура ответа API"); + } + } catch (error) { + console.error("Error fetching Models:", error); + throw new Error( + error instanceof Error ? error.message : "Неизвестная ошибка" + ); + } + } + + static sortModelsByPrice( + models: ProcessedModel[], + order: "asc" | "desc" + ): ProcessedModel[] { + return [...models].sort((a, b) => { + const getPriceValue = (price: string) => { + if (price === "Бесплатно") return 0; + const match = price.match(/(\d+\.?\d*)/); + return match ? parseFloat(match[1]) : 0; + }; + + const priceA = getPriceValue(a.price); + const priceB = getPriceValue(b.price); + + return order === "asc" ? priceA - priceB : priceB - priceA; + }); + } + + static filterModels( + models: ProcessedModel[], + searchQuery: string, + sortOrder: "asc" | "desc" + ): ProcessedModel[] { + let filtered = models; + + if (searchQuery.trim()) { + filtered = models.filter((model) => + model.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + } + + return this.sortModelsByPrice(filtered, sortOrder); + } + + static copyModelId(modelId: string): Promise { + return new Promise((resolve) => { + try { + const textArea = document.createElement("textarea"); + textArea.value = modelId; + textArea.style.position = "fixed"; + textArea.style.left = "-999999px"; + textArea.style.top = "-999999px"; + textArea.style.opacity = "0"; + document.body.appendChild(textArea); + + textArea.focus(); + textArea.select(); + textArea.setSelectionRange(0, 99999); + + const successful = document.execCommand("copy"); + textArea.remove(); + + resolve(successful); + } catch (err) { + console.error("Copy error:", err); + resolve(false); + } + }); + } + + static createSuccessSnackbar( + message: string, + onClose: () => void + ): React.ReactNode { + return ( + } + style={{ marginBottom: "60px" }} + > + {message} + + ); + } + + static createErrorSnackbar( + message: string, + onClose: () => void + ): React.ReactNode { + return ( + } + style={{ marginBottom: "60px" }} + > + {message} + + ); + } + + static createTryModelSnackbar( + modelId: string, + onClose: () => void, + onTryModel?: (modelId: string) => void + ): React.ReactNode { + return ( + 🚀
} + style={{ marginBottom: "60px" }} + action={ + onTryModel ? ( + + ) : undefined + } + > + Модель: {modelId} + + ); + } +} + + diff --git a/GPTutor-Frontend-v2/src/services/ProfileService.tsx b/GPTutor-Frontend-v2/src/services/ProfileService.tsx new file mode 100644 index 00000000..9296454e --- /dev/null +++ b/GPTutor-Frontend-v2/src/services/ProfileService.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { Snackbar } from "@vkontakte/vkui"; +import { Icon28RefreshOutline, Icon28SettingsOutline } from "@vkontakte/icons"; +import { profileApi, UserProfile, VkData } from "../api/profileApi"; + +export class ProfileService { + static async loadProfile(): Promise<{ user: UserProfile; vkData: VkData }> { + try { + const response = await profileApi.getProfile(); + return { + user: response.user, + vkData: response.vkData, + }; + } catch (error) { + console.error("Failed to load profile:", error); + throw new Error("Ошибка загрузки профиля"); + } + } + + static async updateApiToken(): Promise { + try { + const response = await profileApi.updateToken(); + return response.newApiKey; + } catch (error) { + console.error("Failed to update token:", error); + throw new Error("Ошибка обновления ключа"); + } + } + + static createSuccessSnackbar( + message: string, + onClose: () => void + ): React.ReactNode { + return ( + }> + {message} + + ); + } + + static createErrorSnackbar( + message: string, + onClose: () => void + ): React.ReactNode { + return ( + }> + {message} + + ); + } +} + + diff --git a/GPTutor-Frontend-v2/src/styles/prism.css b/GPTutor-Frontend-v2/src/styles/prism.css new file mode 100644 index 00000000..7b16268d --- /dev/null +++ b/GPTutor-Frontend-v2/src/styles/prism.css @@ -0,0 +1,147 @@ +/* Prism.js theme - VS Code Dark */ +code[class*="language-"], +pre[class*="language-"] { + color: #d4d4d4; + background: none; + font-family: 'Fira Code', 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: 13px; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.6; + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 16px; + margin: 0; + overflow: auto; + background: #1e1e1e; + border-radius: 8px; + border: 1px solid #333; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + padding: 2px 4px; + border-radius: 4px; + background: #1e1e1e; + color: #d4d4d4; +} + +/* Token colors */ +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: #6a9955; + font-style: italic; +} + +.token.punctuation { + color: #d4d4d4; +} + +.token.property, +.token.tag, +.token.boolean, +.token.number, +.token.constant, +.token.symbol, +.token.deleted { + color: #b5cea8; +} + +.token.selector, +.token.attr-name, +.token.string, +.token.char, +.token.builtin, +.token.inserted { + color: #ce9178; +} + +.token.operator, +.token.entity, +.token.url, +.language-css .token.string, +.style .token.string { + color: #d4d4d4; +} + +.token.atrule, +.token.attr-value, +.token.keyword { + color: #569cd6; +} + +.token.function, +.token.class-name { + color: #dcdcaa; +} + +.token.regex, +.token.important, +.token.variable { + color: #d16969; +} + +/* Code buttons */ +.code-buttons { + position: absolute; + top: 8px; + right: 8px; + display: flex; + gap: 8px; + opacity: 0; + transition: opacity 0.2s ease; +} + +[data-pre-container]:hover .code-buttons { + opacity: 1; +} + +.code-buttons button { + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + color: #d4d4d4; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + cursor: pointer; + transition: all 0.2s ease; +} + +.code-buttons button:hover { + background: rgba(255, 255, 255, 0.2); + border-color: rgba(255, 255, 255, 0.3); +} + +/* Language specific styles */ +.language-bash .token.function { + color: #ce9178; +} + +.language-python .token.keyword { + color: #569cd6; +} + + + + + + + + + + + diff --git a/GPTutor-Frontend-v2/src/themes/OneDark.tsx b/GPTutor-Frontend-v2/src/themes/OneDark.tsx new file mode 100644 index 00000000..4dd2a707 --- /dev/null +++ b/GPTutor-Frontend-v2/src/themes/OneDark.tsx @@ -0,0 +1,461 @@ +export function OneDark() { + return ( + + ); +} diff --git a/GPTutor-Frontend-v2/src/themes/OneLight.tsx b/GPTutor-Frontend-v2/src/themes/OneLight.tsx new file mode 100644 index 00000000..870b9750 --- /dev/null +++ b/GPTutor-Frontend-v2/src/themes/OneLight.tsx @@ -0,0 +1,440 @@ +export function OneLight() { + return ( + + ); +} diff --git a/GPTutor-Frontend-v2/src/utils/citationFormatter.ts b/GPTutor-Frontend-v2/src/utils/citationFormatter.ts new file mode 100644 index 00000000..170cdb39 --- /dev/null +++ b/GPTutor-Frontend-v2/src/utils/citationFormatter.ts @@ -0,0 +1,53 @@ +/** + * Утилита для форматирования текста с цитированиями + */ + +/** + * Заменяет [N] в тексте на кликабельные ссылки + * @param text Исходный текст с маркерами [1], [2] и т.д. + * @param citations Массив URL для замены + * @returns HTML строка с замененными ссылками + */ +export function formatTextWithCitations(text: string, citations: string[]): string { + if (!citations || citations.length === 0) { + return text; + } + + let formattedText = text; + + // Заменяем каждое вхождение [N] на ссылку + citations.forEach((url, index) => { + const citationNumber = index + 1; + const citationPattern = new RegExp(`\\[${citationNumber}\\]`, 'g'); + + // Создаем HTML для ссылки + const citationLink = `[${citationNumber}]`; + + formattedText = formattedText.replace(citationPattern, citationLink); + }); + + return formattedText; +} + +/** + * Получает домен из URL для отображения + */ +export function getDomainFromUrl(url: string): string { + try { + const urlObj = new URL(url); + return urlObj.hostname.replace('www.', ''); + } catch { + return url; + } +} + +/** + * Форматирует заголовок цитирования + */ +export function formatCitationTitle(title: string, url: string): string { + if (title && title !== url) { + return title; + } + return getDomainFromUrl(url); +} + diff --git a/GPTutor-Frontend-v2/src/utils/codeFormatter.ts b/GPTutor-Frontend-v2/src/utils/codeFormatter.ts new file mode 100644 index 00000000..6468e419 --- /dev/null +++ b/GPTutor-Frontend-v2/src/utils/codeFormatter.ts @@ -0,0 +1,21 @@ +import Markdown from '../services/Markdown'; + +const markdownService = new Markdown(); + +// Функция для форматирования кода с подсветкой синтаксиса +export const formatCode = (code: string, language: string = 'javascript'): string => { + const codeBlock = `\`\`\`${language}\n${code}\n\`\`\``; + return markdownService.render(codeBlock); +}; + +// Функция для создания HTML с подсветкой синтаксиса +export const createCodeHTML = (code: string, language: string = 'javascript'): string => { + return formatCode(code, language); +}; + +// Функция для получения CSS классов для подсветки +export const getCodeStyles = () => { + return ` + + `; +}; diff --git a/GPTutor-Frontend-v2/src/utils/index.ts b/GPTutor-Frontend-v2/src/utils/index.ts new file mode 100644 index 00000000..23479a90 --- /dev/null +++ b/GPTutor-Frontend-v2/src/utils/index.ts @@ -0,0 +1 @@ +export { transformVKBridgeAdaptivity } from "./transformVKBridgeAdaptivity.ts"; diff --git a/GPTutor-Frontend-v2/src/utils/transformVKBridgeAdaptivity.ts b/GPTutor-Frontend-v2/src/utils/transformVKBridgeAdaptivity.ts new file mode 100644 index 00000000..194ed2d1 --- /dev/null +++ b/GPTutor-Frontend-v2/src/utils/transformVKBridgeAdaptivity.ts @@ -0,0 +1,31 @@ +import { + type AdaptivityProps, + getViewWidthByViewportWidth, + getViewHeightByViewportHeight, + ViewWidth, + SizeType, +} from '@vkontakte/vkui'; +import type { UseAdaptivity } from '@vkontakte/vk-bridge-react'; + +export const transformVKBridgeAdaptivity = ({ + type, + viewportWidth, + viewportHeight, +}: UseAdaptivity): AdaptivityProps => { + switch (type) { + case 'adaptive': + return { + viewWidth: getViewWidthByViewportWidth(viewportWidth), + viewHeight: getViewHeightByViewportHeight(viewportHeight), + }; + case 'force_mobile': + case 'force_mobile_compact': + return { + viewWidth: ViewWidth.MOBILE, + sizeX: SizeType.COMPACT, + sizeY: type === 'force_mobile_compact' ? SizeType.COMPACT : SizeType.REGULAR, + }; + default: + return {}; + } +}; diff --git a/GPTutor-Frontend-v2/src/viewModels/ModelsViewModel.ts b/GPTutor-Frontend-v2/src/viewModels/ModelsViewModel.ts new file mode 100644 index 00000000..65ff399b --- /dev/null +++ b/GPTutor-Frontend-v2/src/viewModels/ModelsViewModel.ts @@ -0,0 +1,120 @@ +import { useCallback, useEffect, useState } from "react"; +import { ProcessedModel } from "../api"; +import { ModelsService } from "../services/ModelsService"; +import { chatViewModel } from "../panels/Chat/models"; +import { useSnackbar } from "../hooks"; + +export interface ModelsViewModelState { + models: ProcessedModel[]; + filteredModels: ProcessedModel[]; + searchQuery: string; + sortOrder: "asc" | "desc"; + loading: boolean; +} + +export const useModelsViewModel = () => { + const [state, setState] = useState({ + models: [], + filteredModels: [], + searchQuery: "", + sortOrder: "asc", + loading: false, + }); + const { showSuccess, showError } = useSnackbar(); + + const setSearchQuery = useCallback((searchQuery: string) => { + setState((prev) => ({ ...prev, searchQuery })); + }, []); + + const setSortOrder = useCallback((sortOrder: "asc" | "desc") => { + setState((prev) => ({ ...prev, sortOrder })); + }, []); + + const filterModels = useCallback((query: string) => { + setState((prev) => { + const filtered = ModelsService.filterModels( + prev.models, + query, + prev.sortOrder + ); + return { + ...prev, + filteredModels: filtered, + }; + }); + }, []); + + const handleSearchChange = useCallback( + (event: React.ChangeEvent) => { + const value = event.target.value; + setSearchQuery(value); + filterModels(value); + }, + [filterModels, setSearchQuery] + ); + + const handleQuickSearch = useCallback( + (query: string) => { + setSearchQuery(query); + filterModels(query); + }, + [filterModels, setSearchQuery] + ); + + const handleSortToggle = useCallback(() => { + const newOrder = state.sortOrder === "asc" ? "desc" : "asc"; + setSortOrder(newOrder); + }, [state.sortOrder, setSortOrder]); + + const copyModelId = useCallback(async (modelId: string) => { + const success = await ModelsService.copyModelId(modelId); + + if (success) { + showSuccess(`ID скопирован: ${modelId}`); + } else { + showError("Не удалось скопировать"); + } + }, []); + + const tryModel = useCallback((modelId: string) => { + chatViewModel.setModel(modelId); + }, []); + + const loadModels = useCallback(async () => { + try { + setState((prev) => ({ ...prev, loading: true })); + const models = await ModelsService.fetchModels(); + setState((prev) => ({ + ...prev, + models, + filteredModels: models, + loading: false, + })); + } catch (error) { + showError( + `Ошибка загрузки моделей: ${ + error instanceof Error ? error.message : "Неизвестная ошибка" + }` + ); + + setState((prev) => ({ + ...prev, + loading: false, + })); + } + }, []); + + useEffect(() => { + filterModels(state.searchQuery); + }, [state.sortOrder, filterModels, state.searchQuery]); + + return { + ...state, + handleSearchChange, + handleQuickSearch, + handleSortToggle, + copyModelId, + tryModel, + loadModels, + }; +}; diff --git a/GPTutor-Frontend-v2/src/viewModels/ProfileViewModel.ts b/GPTutor-Frontend-v2/src/viewModels/ProfileViewModel.ts new file mode 100644 index 00000000..ed190c03 --- /dev/null +++ b/GPTutor-Frontend-v2/src/viewModels/ProfileViewModel.ts @@ -0,0 +1,80 @@ +import { useState, useCallback } from "react"; +import { UserProfile, VkData } from "../api/profileApi"; +import { ProfileService } from "../services/ProfileService"; +import { CodeExampleType } from "../services/CodeExampleService"; +import { useSnackbar } from "../hooks"; + +export interface ProfileViewModelState { + profile: UserProfile | null; + vkData: VkData | null; + loading: boolean; + updatingToken: boolean; + activeCodeExample: CodeExampleType; +} + +export const useProfileViewModel = () => { + const { showSuccess, showError } = useSnackbar(); + + const [state, setState] = useState({ + profile: null, + vkData: null, + loading: true, + updatingToken: false, + activeCodeExample: "curl", + }); + + const setActiveCodeExample = useCallback( + (activeCodeExample: CodeExampleType) => { + setState((prev) => ({ ...prev, activeCodeExample })); + }, + [] + ); + + const loadProfile = useCallback(async () => { + try { + setState((prev) => ({ ...prev, loading: true })); + const { user, vkData } = await ProfileService.loadProfile(); + setState((prev) => ({ + ...prev, + profile: user, + vkData, + loading: false, + })); + } catch (error) { + showError("Ошибка загрузки профиля"); + + setState((prev) => ({ + ...prev, + loading: false, + })); + } + }, []); + + const updateToken = useCallback(async () => { + try { + setState((prev) => ({ ...prev, updatingToken: true })); + const newApiKey = await ProfileService.updateApiToken(); + + setState((prev) => ({ + ...prev, + profile: prev.profile ? { ...prev.profile, apiKey: newApiKey } : null, + updatingToken: false, + })); + + showSuccess("API ключ успешно обновлен!"); + } catch (error) { + showError("Ошибка обновления ключа"); + setState((prev) => ({ + ...prev, + updatingToken: false, + })); + } + }, []); + + return { + ...state, + setActiveCodeExample, + loadProfile, + updateToken, + }; +}; diff --git a/GPTutor-Frontend-v2/src/viewModels/UserViewModel.ts b/GPTutor-Frontend-v2/src/viewModels/UserViewModel.ts new file mode 100644 index 00000000..7e84b321 --- /dev/null +++ b/GPTutor-Frontend-v2/src/viewModels/UserViewModel.ts @@ -0,0 +1,27 @@ +import bridge, { UserInfo } from "@vkontakte/vk-bridge"; +import { makeAutoObservable } from "mobx"; + +export class UserViewModel { + loading = false; + user: UserInfo = {} as UserInfo; + + constructor() { + makeAutoObservable(this); + } + + async getUser() { + try { + this.loading = true; + this.user = await bridge.send("VKWebAppGetUserInfo"); + console.log(this.user); + } finally { + this.loading = false; + } + } + + getUserId() { + return this.user.id; + } +} + +export const userViewModel = new UserViewModel(); diff --git a/GPTutor-Frontend-v2/src/vite-env.d.ts b/GPTutor-Frontend-v2/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/GPTutor-Frontend-v2/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/GPTutor-Frontend-v2/tsconfig.json b/GPTutor-Frontend-v2/tsconfig.json new file mode 100644 index 00000000..d05b11b5 --- /dev/null +++ b/GPTutor-Frontend-v2/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": [ + "src" + ], + "references": [{ "path": "./tsconfig.node.json"}] +} diff --git a/GPTutor-Frontend-v2/tsconfig.node.json b/GPTutor-Frontend-v2/tsconfig.node.json new file mode 100644 index 00000000..b5a34318 --- /dev/null +++ b/GPTutor-Frontend-v2/tsconfig.node.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": [ + "vite.config.ts" + ] +} diff --git a/GPTutor-Frontend-v2/vite.config.ts b/GPTutor-Frontend-v2/vite.config.ts new file mode 100644 index 00000000..8f045c4d --- /dev/null +++ b/GPTutor-Frontend-v2/vite.config.ts @@ -0,0 +1,38 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import legacy from "@vitejs/plugin-legacy"; + +function handleModuleDirectivesPlugin() { + return { + name: "handle-module-directives-plugin", + transform(code, id) { + if (id.includes("@vkontakte/icons")) { + code = code.replace(/"use-client";?/g, ""); + } + return { code }; + }, + }; +} + +/** + * Some chunks may be large. + * This will not affect the loading speed of the site. + * We collect several versions of scripts that are applied depending on the browser version. + * This is done so that your code runs equally well on the site and in the odr. + * The details are here: https://dev.vk.com/mini-apps/development/on-demand-resources. + */ +export default defineConfig({ + base: "./", + + plugins: [ + react(), + handleModuleDirectivesPlugin(), + legacy({ + targets: ["defaults", "not IE 11"], + }), + ], + server: {}, + build: { + outDir: "build", + }, +}); diff --git a/GPTutor-Frontend-v2/vk-hosting-config.json b/GPTutor-Frontend-v2/vk-hosting-config.json new file mode 100644 index 00000000..7d39a211 --- /dev/null +++ b/GPTutor-Frontend-v2/vk-hosting-config.json @@ -0,0 +1,9 @@ +{ + "static_path": "build", + "app_id": 0, + "endpoints": { + "mobile": "index.html", + "mvk": "index.html", + "web": "index.html" + } +} diff --git a/GPTutor-Frontend/package-lock.json b/GPTutor-Frontend/package-lock.json index 62d85738..ada879fc 100644 --- a/GPTutor-Frontend/package-lock.json +++ b/GPTutor-Frontend/package-lock.json @@ -17,14 +17,14 @@ "@monaco-editor/react": "^4.5.1", "@telegram-apps/sdk": "^1.1.3", "@types/node": "^12.0.0", - "@types/react": "^18.0.0", - "@types/react-dom": "^18.0.0", + "@types/react": "^18.0.28", + "@types/react-dom": "^18.0.28", "@uiw/react-codemirror": "^4.21.7", "@vkontakte/icons": "2.98.0", "@vkontakte/vk-bridge": "^2.14.1", "@vkontakte/vk-bridge-react": "^1.0.1", "@vkontakte/vk-miniapps-deploy": "0.0.25", - "@vkontakte/vkui": "6.0.2", + "@vkontakte/vkui": "6.7.4", "babel-eslint": "^10.1.0", "cross-env": "7.0.3", "dignals": "0.1.0", @@ -87,6 +87,7 @@ "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -2424,6 +2425,7 @@ }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", + "dev": true, "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.3.0" @@ -2437,6 +2439,7 @@ }, "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { "version": "3.4.0", + "dev": true, "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2449,6 +2452,7 @@ "version": "4.8.0", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.0.tgz", "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==", + "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -2457,6 +2461,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -2478,12 +2483,14 @@ "node_modules/@eslint/eslintrc/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "13.21.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "dev": true, "dependencies": { "type-fest": "^0.20.2" }, @@ -2498,6 +2505,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -2509,6 +2517,7 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, "engines": { "node": ">=10" }, @@ -2520,33 +2529,37 @@ "version": "8.48.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.48.0.tgz", "integrity": "sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==", + "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@floating-ui/core": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", - "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.1" + "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/dom": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz", - "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.0.0", - "@floating-ui/utils": "^0.2.0" + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz", - "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.6.1" + "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", @@ -2554,9 +2567,10 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", - "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" }, "node_modules/@happysanta/router": { "version": "0.3.1", @@ -2579,6 +2593,7 @@ "version": "0.11.11", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", @@ -2590,6 +2605,7 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=12.22" @@ -2602,7 +2618,8 @@ "node_modules/@humanwhocodes/object-schema": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", @@ -3527,6 +3544,7 @@ }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -3538,6 +3556,7 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -3545,6 +3564,7 @@ }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -4024,11 +4044,12 @@ } }, "node_modules/@swc/helpers": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", - "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", "dependencies": { - "tslib": "^2.4.0" + "tslib": "^2.8.0" } }, "node_modules/@telegram-apps/sdk": { @@ -5352,27 +5373,38 @@ } }, "node_modules/@vkontakte/vkjs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@vkontakte/vkjs/-/vkjs-1.1.1.tgz", - "integrity": "sha512-+tOfx6M+tsHweG+2xEHUUA5SAWbA2dYYX8Owa6uQ8OpwDq6LCgGikCas2SWDA61N5IXerfbfhDaKwFTuoG2vMg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@vkontakte/vkjs/-/vkjs-1.3.0.tgz", + "integrity": "sha512-GfwCC2JNetiiFZcEWyK9LOy8PHUKK2+L8VSJMbXKnpi4LnkxXYMEBsun1rEKGliPITYZC50nLm7jhxbCgcxVuw==", + "license": "MIT", "dependencies": { - "@swc/helpers": "^0.5.0" + "@swc/helpers": "^0.5.0", + "clsx": "^2.1.1" }, "engines": { "yarn": "^1.21.1" } }, + "node_modules/@vkontakte/vkjs/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/@vkontakte/vkui": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@vkontakte/vkui/-/vkui-6.0.2.tgz", - "integrity": "sha512-Wnar49W7iUMANz4nK1c/uM+FayqKsYXPF2NAfxv/EvJrnDAVd5Ryv7zIX/+zo1LX7+MomkWPAjjlG9wRdFjbSQ==", + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@vkontakte/vkui/-/vkui-6.7.4.tgz", + "integrity": "sha512-EUpL2k8+veOIkSJw4gDspZ6DQIgXO2PgfwIzpaT8E4zP90j2JcHz1LB91sPyveuAn4AzCmnqHD1pIyAZXbEIkw==", + "license": "MIT", "dependencies": { - "@swc/helpers": "^0.5.3", - "@vkontakte/icons": "^2.62.0", - "@vkontakte/vkjs": "^1.1.1", - "@vkontakte/vkui-floating-ui": "^0.1.2", - "dayjs": "^1.11.10", - "mitt": "^3.0.1" + "@swc/helpers": "^0.5.13", + "@vkontakte/icons": "^2.115.0", + "@vkontakte/vkjs": "^1.3.0", + "@vkontakte/vkui-floating-ui": "^0.2.1", + "date-fns": "^3.6.0" }, "peerDependencies": { "react": "^18.2.0", @@ -5380,18 +5412,43 @@ } }, "node_modules/@vkontakte/vkui-floating-ui": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@vkontakte/vkui-floating-ui/-/vkui-floating-ui-0.1.5.tgz", - "integrity": "sha512-5BJOI3/K+aDMdhpvftbvaAlpXktU9fPkYpG5cOQDKTZ9uissWOc0Kk350vInPWIsH9nlovjswKtVmKl0/fA/mQ==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@vkontakte/vkui-floating-ui/-/vkui-floating-ui-0.2.7.tgz", + "integrity": "sha512-4cHwX2pJI4HBZM+Cp/E4f9tDIxU6AimvDEX5WEHGOLUkbCZDiSB2wavsZz128czedpmA/b+AX/l2codwKOhJtA==", + "license": "MIT", "dependencies": { - "@floating-ui/react-dom": "^2.0.8", - "@swc/helpers": "^0.5.3" + "@floating-ui/react-dom": "^2.1.6", + "@swc/helpers": "^0.5.17" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, + "node_modules/@vkontakte/vkui/node_modules/@vkontakte/icons": { + "version": "2.170.0", + "resolved": "https://registry.npmjs.org/@vkontakte/icons/-/icons-2.170.0.tgz", + "integrity": "sha512-AKGNJ1t99QrPg9BitcWdJ0H4OOHmXAwUWyVDTaR6csf05gYehCltrIyLd80waCzOvUQGi9q/BXgOg0oWTJSGcQ==", + "license": "MIT", + "dependencies": { + "@vkontakte/icons-sprite": "2.3.1" + }, + "peerDependencies": { + "react": "^16.9.34 || ^17 || ^18" + } + }, + "node_modules/@vkontakte/vkui/node_modules/@vkontakte/icons-sprite": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@vkontakte/icons-sprite/-/icons-sprite-2.3.1.tgz", + "integrity": "sha512-j7HB2Mqw2kJdWN/sTgiXDW7hXpsIM6l5MYYJ9OisxtzbitXH1wvl7aEXUc3NuR0JPVg7ImaZeot+HU4VjtMcag==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.15" + }, + "peerDependencies": { + "react": "^16.9.34 || ^17 || ^18" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.1", "dev": true, @@ -5558,6 +5615,7 @@ "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -5597,6 +5655,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -5687,6 +5746,7 @@ }, "node_modules/ajv": { "version": "6.12.6", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -5778,6 +5838,7 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6947,6 +7008,7 @@ }, "node_modules/callsites": { "version": "3.1.0", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -7749,6 +7811,7 @@ }, "node_modules/cross-spawn": { "version": "7.0.3", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -8892,10 +8955,15 @@ "node": ">=10" } }, - "node_modules/dayjs": { - "version": "1.11.10", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", - "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } }, "node_modules/debug": { "version": "4.3.4", @@ -8924,6 +8992,7 @@ }, "node_modules/deep-is": { "version": "0.1.4", + "dev": true, "license": "MIT" }, "node_modules/default-gateway": { @@ -9139,6 +9208,7 @@ }, "node_modules/doctrine": { "version": "3.0.0", + "dev": true, "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" @@ -9587,6 +9657,7 @@ "version": "8.48.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.48.0.tgz", "integrity": "sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==", + "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -10552,10 +10623,12 @@ "node_modules/eslint/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "node_modules/eslint/node_modules/chalk": { "version": "4.1.2", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -10570,6 +10643,7 @@ }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -10582,6 +10656,7 @@ "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -10597,6 +10672,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -10606,6 +10682,7 @@ }, "node_modules/eslint/node_modules/globals": { "version": "13.20.0", + "dev": true, "license": "MIT", "dependencies": { "type-fest": "^0.20.2" @@ -10621,6 +10698,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -10630,6 +10708,7 @@ }, "node_modules/eslint/node_modules/type-fest": { "version": "0.20.2", + "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" @@ -10650,6 +10729,7 @@ "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -10666,6 +10746,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -10687,6 +10768,7 @@ }, "node_modules/esquery": { "version": "1.5.0", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" @@ -10697,6 +10779,7 @@ }, "node_modules/esrecurse": { "version": "4.3.0", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" @@ -10906,6 +10989,7 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", + "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -10936,14 +11020,17 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", + "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", + "dev": true, "license": "MIT" }, "node_modules/fastq": { "version": "1.13.0", + "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -10975,6 +11062,7 @@ }, "node_modules/file-entry-cache": { "version": "6.0.1", + "dev": true, "license": "MIT", "dependencies": { "flat-cache": "^3.0.4" @@ -11117,6 +11205,7 @@ }, "node_modules/find-up": { "version": "5.0.0", + "dev": true, "license": "MIT", "dependencies": { "locate-path": "^6.0.0", @@ -11131,6 +11220,7 @@ }, "node_modules/flat-cache": { "version": "3.0.4", + "dev": true, "license": "MIT", "dependencies": { "flatted": "^3.1.0", @@ -11142,6 +11232,7 @@ }, "node_modules/flatted": { "version": "3.2.7", + "dev": true, "license": "ISC" }, "node_modules/follow-redirects": { @@ -11518,6 +11609,7 @@ }, "node_modules/glob-parent": { "version": "6.0.2", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -11656,7 +11748,8 @@ "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true }, "node_modules/graphlib": { "version": "2.1.8", @@ -12036,6 +12129,7 @@ "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, "engines": { "node": ">= 4" } @@ -12051,6 +12145,7 @@ }, "node_modules/import-fresh": { "version": "3.3.0", + "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -12065,6 +12160,7 @@ }, "node_modules/import-fresh/node_modules/resolve-from": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -12343,6 +12439,7 @@ }, "node_modules/is-path-inside": { "version": "3.0.3", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -12493,6 +12590,7 @@ }, "node_modules/isexe": { "version": "2.0.0", + "dev": true, "license": "ISC" }, "node_modules/isobject": { @@ -14349,10 +14447,12 @@ }, "node_modules/json-schema-traverse": { "version": "0.4.1", + "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", + "dev": true, "license": "MIT" }, "node_modules/json5": { @@ -14481,6 +14581,7 @@ }, "node_modules/levn": { "version": "0.4.1", + "dev": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", @@ -14534,6 +14635,7 @@ }, "node_modules/locate-path": { "version": "6.0.0", + "dev": true, "license": "MIT", "dependencies": { "p-locate": "^5.0.0" @@ -14584,6 +14686,7 @@ }, "node_modules/lodash.merge": { "version": "4.6.2", + "dev": true, "license": "MIT" }, "node_modules/lodash.sortby": { @@ -15021,11 +15124,6 @@ "version": "1.2.6", "license": "MIT" }, - "node_modules/mitt": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" - }, "node_modules/mj-context-menu": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz", @@ -15047,12 +15145,6 @@ "resolved": "https://registry.npmjs.org/moment-mini/-/moment-mini-2.29.4.tgz", "integrity": "sha512-uhXpYwHFeiTbY9KSgPPRoo1nt8OxNVdMVoTBYHfSEKeRkIkwGpO+gERmhuhBtzfaeOyTkykSrm2+noJBgqt3Hg==" }, - "node_modules/monaco-editor": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.46.0.tgz", - "integrity": "sha512-ADwtLIIww+9FKybWscd7OCfm9odsFYHImBRI1v9AviGce55QY8raT+9ihH8jX/E/e6QVSGM+pKj4jSUSRmALNQ==", - "peer": true - }, "node_modules/ms": { "version": "2.1.2", "license": "MIT" @@ -15082,6 +15174,7 @@ }, "node_modules/natural-compare": { "version": "1.4.0", + "dev": true, "license": "MIT" }, "node_modules/natural-compare-lite": { @@ -15713,6 +15806,7 @@ "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, "dependencies": { "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", @@ -15727,6 +15821,7 @@ }, "node_modules/p-limit": { "version": "3.1.0", + "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -15740,6 +15835,7 @@ }, "node_modules/p-locate": { "version": "5.0.0", + "dev": true, "license": "MIT", "dependencies": { "p-limit": "^3.0.2" @@ -15782,6 +15878,7 @@ }, "node_modules/parent-module": { "version": "1.0.1", + "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -15838,6 +15935,7 @@ }, "node_modules/path-exists": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -15852,6 +15950,7 @@ }, "node_modules/path-key": { "version": "3.1.1", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -17217,6 +17316,7 @@ }, "node_modules/prelude-ls": { "version": "1.2.1", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8.0" @@ -17367,6 +17467,7 @@ }, "node_modules/punycode": { "version": "2.1.1", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -17424,6 +17525,7 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", + "dev": true, "funding": [ { "type": "github", @@ -18258,6 +18360,7 @@ }, "node_modules/reusify": { "version": "1.0.4", + "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -18266,6 +18369,7 @@ }, "node_modules/rimraf": { "version": "3.0.2", + "dev": true, "license": "ISC", "dependencies": { "glob": "^7.1.3" @@ -18333,6 +18437,7 @@ }, "node_modules/run-parallel": { "version": "1.2.0", + "dev": true, "funding": [ { "type": "github", @@ -18659,6 +18764,7 @@ }, "node_modules/shebang-command": { "version": "2.0.0", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -18669,6 +18775,7 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -19153,6 +19260,7 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -19187,6 +19295,7 @@ }, "node_modules/strip-json-comments": { "version": "3.1.1", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -19605,6 +19714,7 @@ }, "node_modules/text-table": { "version": "0.2.0", + "dev": true, "license": "MIT" }, "node_modules/throat": { @@ -19811,7 +19921,9 @@ } }, "node_modules/tslib": { - "version": "2.4.0", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/tsutils": { @@ -19835,6 +19947,7 @@ }, "node_modules/type-check": { "version": "0.4.0", + "dev": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" @@ -20029,6 +20142,7 @@ }, "node_modules/uri-js": { "version": "4.4.1", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -20619,6 +20733,7 @@ }, "node_modules/which": { "version": "2.0.2", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -21132,6 +21247,7 @@ }, "node_modules/yocto-queue": { "version": "0.1.0", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -21176,7 +21292,8 @@ "@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==" + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true }, "@ampproject/remapping": { "version": "2.1.2", @@ -22326,8 +22443,7 @@ } }, "@bugsnag/plugin-react": { - "version": "7.19.0", - "requires": {} + "version": "7.19.0" }, "@bugsnag/safe-json-stringify": { "version": "6.0.0" @@ -22550,34 +22666,36 @@ }, "@csstools/postcss-unset-value": { "version": "1.0.2", - "dev": true, - "requires": {} + "dev": true }, "@csstools/selector-specificity": { "version": "2.0.2", - "dev": true, - "requires": {} + "dev": true }, "@eslint-community/eslint-utils": { "version": "4.4.0", + "dev": true, "requires": { "eslint-visitor-keys": "^3.3.0" }, "dependencies": { "eslint-visitor-keys": { - "version": "3.4.0" + "version": "3.4.0", + "dev": true } } }, "@eslint-community/regexpp": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.0.tgz", - "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==" + "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==", + "dev": true }, "@eslint/eslintrc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -22593,12 +22711,14 @@ "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "globals": { "version": "13.21.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "dev": true, "requires": { "type-fest": "^0.20.2" } @@ -22607,6 +22727,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, "requires": { "argparse": "^2.0.1" } @@ -22614,44 +22735,46 @@ "type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true } } }, "@eslint/js": { "version": "8.48.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.48.0.tgz", - "integrity": "sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==" + "integrity": "sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==", + "dev": true }, "@floating-ui/core": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", - "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", "requires": { - "@floating-ui/utils": "^0.2.1" + "@floating-ui/utils": "^0.2.10" } }, "@floating-ui/dom": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz", - "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", "requires": { - "@floating-ui/core": "^1.0.0", - "@floating-ui/utils": "^0.2.0" + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" } }, "@floating-ui/react-dom": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz", - "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", "requires": { - "@floating-ui/dom": "^1.6.1" + "@floating-ui/dom": "^1.7.4" } }, "@floating-ui/utils": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", - "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" }, "@happysanta/router": { "version": "0.3.1", @@ -22670,6 +22793,7 @@ "version": "0.11.11", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "dev": true, "requires": { "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", @@ -22677,12 +22801,14 @@ } }, "@humanwhocodes/module-importer": { - "version": "1.0.1" + "version": "1.0.1", + "dev": true }, "@humanwhocodes/object-schema": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", @@ -23340,16 +23466,19 @@ }, "@nodelib/fs.scandir": { "version": "2.1.5", + "dev": true, "requires": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "@nodelib/fs.stat": { - "version": "2.0.5" + "version": "2.0.5", + "dev": true }, "@nodelib/fs.walk": { "version": "1.2.8", + "dev": true, "requires": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -23607,11 +23736,11 @@ } }, "@swc/helpers": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", - "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", "requires": { - "tslib": "^2.4.0" + "tslib": "^2.8.0" } }, "@telegram-apps/sdk": { @@ -24483,8 +24612,7 @@ "@vkontakte/vk-bridge-react": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@vkontakte/vk-bridge-react/-/vk-bridge-react-1.0.1.tgz", - "integrity": "sha512-+r0zxh1UUDcok2VtsPDc6jWHQezfsqHK3M9LAnTpKI9XyZlrgfEQA8cYsc0F8urOXUJ3FhEHXuJQjQGgK0Z5PA==", - "requires": {} + "integrity": "sha512-+r0zxh1UUDcok2VtsPDc6jWHQezfsqHK3M9LAnTpKI9XyZlrgfEQA8cYsc0F8urOXUJ3FhEHXuJQjQGgK0Z5PA==" }, "@vkontakte/vk-miniapps-deploy": { "version": "0.0.25", @@ -24502,33 +24630,58 @@ } }, "@vkontakte/vkjs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@vkontakte/vkjs/-/vkjs-1.1.1.tgz", - "integrity": "sha512-+tOfx6M+tsHweG+2xEHUUA5SAWbA2dYYX8Owa6uQ8OpwDq6LCgGikCas2SWDA61N5IXerfbfhDaKwFTuoG2vMg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@vkontakte/vkjs/-/vkjs-1.3.0.tgz", + "integrity": "sha512-GfwCC2JNetiiFZcEWyK9LOy8PHUKK2+L8VSJMbXKnpi4LnkxXYMEBsun1rEKGliPITYZC50nLm7jhxbCgcxVuw==", "requires": { - "@swc/helpers": "^0.5.0" + "@swc/helpers": "^0.5.0", + "clsx": "^2.1.1" + }, + "dependencies": { + "clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" + } } }, "@vkontakte/vkui": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@vkontakte/vkui/-/vkui-6.0.2.tgz", - "integrity": "sha512-Wnar49W7iUMANz4nK1c/uM+FayqKsYXPF2NAfxv/EvJrnDAVd5Ryv7zIX/+zo1LX7+MomkWPAjjlG9wRdFjbSQ==", + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@vkontakte/vkui/-/vkui-6.7.4.tgz", + "integrity": "sha512-EUpL2k8+veOIkSJw4gDspZ6DQIgXO2PgfwIzpaT8E4zP90j2JcHz1LB91sPyveuAn4AzCmnqHD1pIyAZXbEIkw==", "requires": { - "@swc/helpers": "^0.5.3", - "@vkontakte/icons": "^2.62.0", - "@vkontakte/vkjs": "^1.1.1", - "@vkontakte/vkui-floating-ui": "^0.1.2", - "dayjs": "^1.11.10", - "mitt": "^3.0.1" + "@swc/helpers": "^0.5.13", + "@vkontakte/icons": "^2.115.0", + "@vkontakte/vkjs": "^1.3.0", + "@vkontakte/vkui-floating-ui": "^0.2.1", + "date-fns": "^3.6.0" + }, + "dependencies": { + "@vkontakte/icons": { + "version": "2.170.0", + "resolved": "https://registry.npmjs.org/@vkontakte/icons/-/icons-2.170.0.tgz", + "integrity": "sha512-AKGNJ1t99QrPg9BitcWdJ0H4OOHmXAwUWyVDTaR6csf05gYehCltrIyLd80waCzOvUQGi9q/BXgOg0oWTJSGcQ==", + "requires": { + "@vkontakte/icons-sprite": "2.3.1" + } + }, + "@vkontakte/icons-sprite": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@vkontakte/icons-sprite/-/icons-sprite-2.3.1.tgz", + "integrity": "sha512-j7HB2Mqw2kJdWN/sTgiXDW7hXpsIM6l5MYYJ9OisxtzbitXH1wvl7aEXUc3NuR0JPVg7ImaZeot+HU4VjtMcag==", + "requires": { + "@swc/helpers": "^0.5.15" + } + } } }, "@vkontakte/vkui-floating-ui": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@vkontakte/vkui-floating-ui/-/vkui-floating-ui-0.1.5.tgz", - "integrity": "sha512-5BJOI3/K+aDMdhpvftbvaAlpXktU9fPkYpG5cOQDKTZ9uissWOc0Kk350vInPWIsH9nlovjswKtVmKl0/fA/mQ==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@vkontakte/vkui-floating-ui/-/vkui-floating-ui-0.2.7.tgz", + "integrity": "sha512-4cHwX2pJI4HBZM+Cp/E4f9tDIxU6AimvDEX5WEHGOLUkbCZDiSB2wavsZz128czedpmA/b+AX/l2codwKOhJtA==", "requires": { - "@floating-ui/react-dom": "^2.0.8", - "@swc/helpers": "^0.5.3" + "@floating-ui/react-dom": "^2.1.6", + "@swc/helpers": "^0.5.17" } }, "@webassemblyjs/ast": { @@ -24673,7 +24826,8 @@ "acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==" + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true }, "acorn-globals": { "version": "6.0.0", @@ -24691,14 +24845,13 @@ }, "acorn-import-assertions": { "version": "1.8.0", - "dev": true, - "requires": {} + "dev": true }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "requires": {} + "dev": true }, "acorn-node": { "version": "1.8.2", @@ -24755,6 +24908,7 @@ }, "ajv": { "version": "6.12.6", + "dev": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -24787,8 +24941,7 @@ }, "ajv-keywords": { "version": "3.5.2", - "dev": true, - "requires": {} + "dev": true }, "ansi-colors": { "version": "4.1.3", @@ -24807,7 +24960,8 @@ "dev": true }, "ansi-regex": { - "version": "5.0.1" + "version": "5.0.1", + "dev": true }, "ansi-styles": { "version": "4.3.0", @@ -25346,8 +25500,7 @@ }, "babel-plugin-named-asset-import": { "version": "0.3.8", - "dev": true, - "requires": {} + "dev": true }, "babel-plugin-polyfill-corejs2": { "version": "0.3.2", @@ -25590,7 +25743,8 @@ } }, "callsites": { - "version": "3.1.0" + "version": "3.1.0", + "dev": true }, "camel-case": { "version": "4.1.2", @@ -26098,6 +26252,7 @@ }, "cross-spawn": { "version": "7.0.3", + "dev": true, "requires": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -26116,8 +26271,7 @@ }, "css-declaration-sorter": { "version": "6.3.1", - "dev": true, - "requires": {} + "dev": true }, "css-has-pseudo": { "version": "3.0.4", @@ -26200,8 +26354,7 @@ }, "css-prefers-color-scheme": { "version": "6.0.3", - "dev": true, - "requires": {} + "dev": true }, "css-select": { "version": "4.3.0", @@ -26288,8 +26441,7 @@ }, "cssnano-utils": { "version": "3.1.0", - "dev": true, - "requires": {} + "dev": true }, "csso": { "version": "4.2.0", @@ -26936,10 +27088,10 @@ } } }, - "dayjs": { - "version": "1.11.10", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", - "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + "date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==" }, "debug": { "version": "4.3.4", @@ -26956,7 +27108,8 @@ "dev": true }, "deep-is": { - "version": "0.1.4" + "version": "0.1.4", + "dev": true }, "default-gateway": { "version": "6.0.3", @@ -27060,8 +27213,7 @@ "dignals-model": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/dignals-model/-/dignals-model-0.1.0.tgz", - "integrity": "sha512-+LmMZ4JUWL8/pniuF7avMfhzr4inJYm386JmAOiM7eBYLwN541kL9LnOQFcY65JRI+u6X0Gt+/JMK4tAZtctfQ==", - "requires": {} + "integrity": "sha512-+LmMZ4JUWL8/pniuF7avMfhzr4inJYm386JmAOiM7eBYLwN541kL9LnOQFcY65JRI+u6X0Gt+/JMK4tAZtctfQ==" }, "dignals-react": { "version": "0.1.27", @@ -27097,6 +27249,7 @@ }, "doctrine": { "version": "3.0.0", + "dev": true, "requires": { "esutils": "^2.0.2" } @@ -27397,6 +27550,7 @@ "version": "8.48.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.48.0.tgz", "integrity": "sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==", + "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -27440,22 +27594,26 @@ "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "chalk": { "version": "4.1.2", + "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "escape-string-regexp": { - "version": "4.0.0" + "version": "4.0.0", + "dev": true }, "eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, "requires": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -27464,10 +27622,12 @@ "eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==" + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true }, "globals": { "version": "13.20.0", + "dev": true, "requires": { "type-fest": "^0.20.2" } @@ -27476,12 +27636,14 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, "requires": { "argparse": "^2.0.1" } }, "type-fest": { - "version": "0.20.2" + "version": "0.20.2", + "dev": true } } }, @@ -27799,8 +27961,7 @@ } }, "eslint-plugin-promise": { - "version": "6.1.1", - "requires": {} + "version": "6.1.1" }, "eslint-plugin-react": { "version": "7.32.2", @@ -27948,12 +28109,10 @@ }, "eslint-plugin-react-hooks": { "version": "4.6.0", - "dev": true, - "requires": {} + "dev": true }, "eslint-plugin-standard": { - "version": "5.0.0", - "requires": {} + "version": "5.0.0" }, "eslint-plugin-testing-library": { "version": "5.6.2", @@ -28048,6 +28207,7 @@ "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, "requires": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -28057,7 +28217,8 @@ "eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==" + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true } } }, @@ -28067,12 +28228,14 @@ }, "esquery": { "version": "1.5.0", + "dev": true, "requires": { "estraverse": "^5.1.0" } }, "esrecurse": { "version": "4.3.0", + "dev": true, "requires": { "estraverse": "^5.2.0" } @@ -28215,7 +28378,8 @@ } }, "fast-deep-equal": { - "version": "3.1.3" + "version": "3.1.3", + "dev": true }, "fast-glob": { "version": "3.2.11", @@ -28238,13 +28402,16 @@ } }, "fast-json-stable-stringify": { - "version": "2.1.0" + "version": "2.1.0", + "dev": true }, "fast-levenshtein": { - "version": "2.0.6" + "version": "2.0.6", + "dev": true }, "fastq": { "version": "1.13.0", + "dev": true, "requires": { "reusify": "^1.0.4" } @@ -28270,6 +28437,7 @@ }, "file-entry-cache": { "version": "6.0.1", + "dev": true, "requires": { "flat-cache": "^3.0.4" } @@ -28366,6 +28534,7 @@ }, "find-up": { "version": "5.0.0", + "dev": true, "requires": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -28373,13 +28542,15 @@ }, "flat-cache": { "version": "3.0.4", + "dev": true, "requires": { "flatted": "^3.1.0", "rimraf": "^3.0.2" } }, "flatted": { - "version": "3.2.7" + "version": "3.2.7", + "dev": true }, "follow-redirects": { "version": "1.15.1", @@ -28593,6 +28764,7 @@ }, "glob-parent": { "version": "6.0.2", + "dev": true, "requires": { "is-glob": "^4.0.3" } @@ -28685,7 +28857,8 @@ "graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true }, "graphlib": { "version": "2.1.8", @@ -28887,8 +29060,7 @@ }, "icss-utils": { "version": "5.1.0", - "dev": true, - "requires": {} + "dev": true }, "idb": { "version": "7.0.2", @@ -28907,7 +29079,8 @@ "ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==" + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true }, "immer": { "version": "9.0.15", @@ -28915,13 +29088,15 @@ }, "import-fresh": { "version": "3.3.0", + "dev": true, "requires": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" }, "dependencies": { "resolve-from": { - "version": "4.0.0" + "version": "4.0.0", + "dev": true } } }, @@ -29067,7 +29242,8 @@ "version": "2.0.0" }, "is-path-inside": { - "version": "3.0.3" + "version": "3.0.3", + "dev": true }, "is-plain-object": { "version": "2.0.4", @@ -29150,7 +29326,8 @@ "version": "0.0.2" }, "isexe": { - "version": "2.0.0" + "version": "2.0.0", + "dev": true }, "isobject": { "version": "3.0.1", @@ -29792,8 +29969,7 @@ }, "jest-pnp-resolver": { "version": "1.2.2", - "dev": true, - "requires": {} + "dev": true }, "jest-regex-util": { "version": "27.5.1", @@ -30461,10 +30637,12 @@ "dev": true }, "json-schema-traverse": { - "version": "0.4.1" + "version": "0.4.1", + "dev": true }, "json-stable-stringify-without-jsonify": { - "version": "1.0.1" + "version": "1.0.1", + "dev": true }, "json5": { "version": "1.0.2", @@ -30551,6 +30729,7 @@ }, "levn": { "version": "0.4.1", + "dev": true, "requires": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -30587,6 +30766,7 @@ }, "locate-path": { "version": "6.0.0", + "dev": true, "requires": { "p-locate": "^5.0.0" } @@ -30620,7 +30800,8 @@ "dev": true }, "lodash.merge": { - "version": "4.6.2" + "version": "4.6.2", + "dev": true }, "lodash.sortby": { "version": "4.7.0", @@ -30930,11 +31111,6 @@ "minimist": { "version": "1.2.6" }, - "mitt": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" - }, "mj-context-menu": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz", @@ -30952,12 +31128,6 @@ "resolved": "https://registry.npmjs.org/moment-mini/-/moment-mini-2.29.4.tgz", "integrity": "sha512-uhXpYwHFeiTbY9KSgPPRoo1nt8OxNVdMVoTBYHfSEKeRkIkwGpO+gERmhuhBtzfaeOyTkykSrm2+noJBgqt3Hg==" }, - "monaco-editor": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.46.0.tgz", - "integrity": "sha512-ADwtLIIww+9FKybWscd7OCfm9odsFYHImBRI1v9AviGce55QY8raT+9ihH8jX/E/e6QVSGM+pKj4jSUSRmALNQ==", - "peer": true - }, "ms": { "version": "2.1.2" }, @@ -30974,7 +31144,8 @@ "dev": true }, "natural-compare": { - "version": "1.4.0" + "version": "1.4.0", + "dev": true }, "natural-compare-lite": { "version": "1.4.0", @@ -31384,6 +31555,7 @@ "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, "requires": { "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", @@ -31395,12 +31567,14 @@ }, "p-limit": { "version": "3.1.0", + "dev": true, "requires": { "yocto-queue": "^0.1.0" } }, "p-locate": { "version": "5.0.0", + "dev": true, "requires": { "p-limit": "^3.0.2" } @@ -31427,6 +31601,7 @@ }, "parent-module": { "version": "1.0.1", + "dev": true, "requires": { "callsites": "^3.0.0" } @@ -31465,13 +31640,15 @@ } }, "path-exists": { - "version": "4.0.0" + "version": "4.0.0", + "dev": true }, "path-is-absolute": { "version": "1.0.1" }, "path-key": { - "version": "3.1.1" + "version": "3.1.1", + "dev": true }, "path-parse": { "version": "1.0.7" @@ -31602,8 +31779,7 @@ }, "postcss-browser-comments": { "version": "4.0.0", - "dev": true, - "requires": {} + "dev": true }, "postcss-calc": { "version": "8.2.4", @@ -31689,23 +31865,19 @@ }, "postcss-discard-comments": { "version": "5.1.2", - "dev": true, - "requires": {} + "dev": true }, "postcss-discard-duplicates": { "version": "5.1.0", - "dev": true, - "requires": {} + "dev": true }, "postcss-discard-empty": { "version": "5.1.1", - "dev": true, - "requires": {} + "dev": true }, "postcss-discard-overridden": { "version": "5.1.0", - "dev": true, - "requires": {} + "dev": true }, "postcss-double-position-gradients": { "version": "3.1.2", @@ -31724,8 +31896,7 @@ }, "postcss-flexbugs-fixes": { "version": "5.0.2", - "dev": true, - "requires": {} + "dev": true }, "postcss-focus-visible": { "version": "6.0.4", @@ -31743,13 +31914,11 @@ }, "postcss-font-variant": { "version": "5.0.0", - "dev": true, - "requires": {} + "dev": true }, "postcss-gap-properties": { "version": "3.0.5", - "dev": true, - "requires": {} + "dev": true }, "postcss-image-set-function": { "version": "4.0.7", @@ -31769,8 +31938,7 @@ }, "postcss-initial": { "version": "4.0.1", - "dev": true, - "requires": {} + "dev": true }, "postcss-js": { "version": "4.0.0", @@ -31815,13 +31983,11 @@ }, "postcss-logical": { "version": "5.0.4", - "dev": true, - "requires": {} + "dev": true }, "postcss-media-minmax": { "version": "5.0.0", - "dev": true, - "requires": {} + "dev": true }, "postcss-merge-longhand": { "version": "5.1.6", @@ -31875,8 +32041,7 @@ }, "postcss-modules-extract-imports": { "version": "3.0.0", - "dev": true, - "requires": {} + "dev": true }, "postcss-modules-local-by-default": { "version": "4.0.0", @@ -31927,8 +32092,7 @@ }, "postcss-normalize-charset": { "version": "5.1.0", - "dev": true, - "requires": {} + "dev": true }, "postcss-normalize-display-values": { "version": "5.1.0", @@ -32009,8 +32173,7 @@ }, "postcss-page-break": { "version": "3.0.4", - "dev": true, - "requires": {} + "dev": true }, "postcss-place": { "version": "7.0.5", @@ -32098,8 +32261,7 @@ }, "postcss-replace-overflow-wrap": { "version": "4.0.0", - "dev": true, - "requires": {} + "dev": true }, "postcss-selector-not": { "version": "6.0.1", @@ -32171,7 +32333,8 @@ "dev": true }, "prelude-ls": { - "version": "1.2.1" + "version": "1.2.1", + "dev": true }, "prettier": { "version": "2.8.7", @@ -32266,7 +32429,8 @@ } }, "punycode": { - "version": "2.1.1" + "version": "2.1.1", + "dev": true }, "punycode.js": { "version": "2.3.1", @@ -32298,7 +32462,8 @@ "dev": true }, "queue-microtask": { - "version": "1.2.3" + "version": "1.2.3", + "dev": true }, "quick-lru": { "version": "5.1.1", @@ -32849,10 +33014,12 @@ "dev": true }, "reusify": { - "version": "1.0.4" + "version": "1.0.4", + "dev": true }, "rimraf": { "version": "3.0.2", + "dev": true, "requires": { "glob": "^7.1.3" } @@ -32899,6 +33066,7 @@ }, "run-parallel": { "version": "1.2.0", + "dev": true, "requires": { "queue-microtask": "^1.2.2" } @@ -33116,12 +33284,14 @@ }, "shebang-command": { "version": "2.0.0", + "dev": true, "requires": { "shebang-regex": "^3.0.0" } }, "shebang-regex": { - "version": "3.0.0" + "version": "3.0.0", + "dev": true }, "shell-quote": { "version": "1.7.3", @@ -33460,6 +33630,7 @@ }, "strip-ansi": { "version": "6.0.1", + "dev": true, "requires": { "ansi-regex": "^5.0.1" } @@ -33477,12 +33648,12 @@ "dev": true }, "strip-json-comments": { - "version": "3.1.1" + "version": "3.1.1", + "dev": true }, "style-loader": { "version": "3.3.1", - "dev": true, - "requires": {} + "dev": true }, "style-mod": { "version": "4.0.3" @@ -33744,7 +33915,8 @@ } }, "text-table": { - "version": "0.2.0" + "version": "0.2.0", + "dev": true }, "throat": { "version": "6.0.2", @@ -33792,8 +33964,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.2.tgz", "integrity": "sha512-Cbu4nIqnEdd+THNEsBdkolnOXhg0I8XteoHaEKgvsxpsbWda4IsUut2c187HxywQCvveojow0Dgw/amxtSKVkQ==", - "dev": true, - "requires": {} + "dev": true }, "ts-jest": { "version": "29.1.0", @@ -33867,7 +34038,9 @@ } }, "tslib": { - "version": "2.4.0" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "tsutils": { "version": "3.21.0", @@ -33884,6 +34057,7 @@ }, "type-check": { "version": "0.4.0", + "dev": true, "requires": { "prelude-ls": "^1.2.1" } @@ -33987,6 +34161,7 @@ }, "uri-js": { "version": "4.4.1", + "dev": true, "requires": { "punycode": "^2.1.0" } @@ -34000,8 +34175,7 @@ } }, "use-sync-external-store": { - "version": "1.2.0", - "requires": {} + "version": "1.2.0" }, "util-deprecate": { "version": "1.0.2" @@ -34295,8 +34469,7 @@ }, "ws": { "version": "8.8.1", - "dev": true, - "requires": {} + "dev": true } } }, @@ -34385,6 +34558,7 @@ }, "which": { "version": "2.0.2", + "dev": true, "requires": { "isexe": "^2.0.0" } @@ -34701,8 +34875,7 @@ }, "ws": { "version": "7.5.9", - "dev": true, - "requires": {} + "dev": true }, "xdg-basedir": { "version": "4.0.0" @@ -34758,7 +34931,8 @@ "dev": true }, "yocto-queue": { - "version": "0.1.0" + "version": "0.1.0", + "dev": true }, "zip-a-folder": { "version": "0.0.12", diff --git a/GPTutor-Frontend/package.json b/GPTutor-Frontend/package.json index 6f051c44..e7013e40 100644 --- a/GPTutor-Frontend/package.json +++ b/GPTutor-Frontend/package.json @@ -22,14 +22,14 @@ "@monaco-editor/react": "^4.5.1", "@telegram-apps/sdk": "^1.1.3", "@types/node": "^12.0.0", - "@types/react": "^18.0.0", - "@types/react-dom": "^18.0.0", + "@types/react": "^18.0.28", + "@types/react-dom": "^18.0.28", "@uiw/react-codemirror": "^4.21.7", "@vkontakte/icons": "2.98.0", "@vkontakte/vk-bridge": "^2.14.1", "@vkontakte/vk-bridge-react": "^1.0.1", "@vkontakte/vk-miniapps-deploy": "0.0.25", - "@vkontakte/vkui": "6.0.2", + "@vkontakte/vkui": "6.7.4", "babel-eslint": "^10.1.0", "cross-env": "7.0.3", "dignals": "0.1.0", diff --git a/GPTutor-Frontend/src/App.tsx b/GPTutor-Frontend/src/App.tsx index f7bc2896..4b3bbb2c 100644 --- a/GPTutor-Frontend/src/App.tsx +++ b/GPTutor-Frontend/src/App.tsx @@ -68,13 +68,14 @@ import { transformVKBridgeAdaptivity } from "$/utility/strings"; import { MermaidPage } from "$/panels/MermaidPage"; import { AdditionalRequests } from "$/panels/AdditionalRequests"; import { AnecdoteMain } from "$/panels/AnecdoteMain"; -import AnecdoteGeneration from "./panels/AnecdoteGeneration/AnecdoteGeneration"; import { AnecdoteNews } from "$/panels/AnecdoteNews"; -import ApplicationInfoHumor from "./modals/ApplicationInfoHumor/ApplicationInfoHumor"; import { BingPanel } from "$/panels/BingPanel"; -import VKDocQuestionPanel from "./panels/VKDocQuestionPanel/VKDocQestionPanel"; import { VkDocQuestionRequest } from "$/panels/VkDocQuestionRequest"; +import AnecdoteGeneration from "./panels/AnecdoteGeneration/AnecdoteGeneration"; import { retrieveLaunchParams } from "@telegram-apps/sdk"; +import ApplicationInfoHumor from "./modals/ApplicationInfoHumor/ApplicationInfoHumor"; +import VKDocQuestionPanel from "./panels/VKDocQuestionPanel/VKDocQestionPanel"; +import DocQuestionPanel from "./panels/ApiLLMPanel"; const App = () => { const location = useLocation(); @@ -102,8 +103,6 @@ const App = () => { window.location.search ); - console.log(vkBridgeAppearance); - function getPlatform() { if (appService.isTG()) { return "vkcom"; @@ -198,6 +197,7 @@ const App = () => { + )} diff --git a/GPTutor-Frontend/src/components/AppContainer/AppContainer.tsx b/GPTutor-Frontend/src/components/AppContainer/AppContainer.tsx index 378c5662..118c7ce0 100644 --- a/GPTutor-Frontend/src/components/AppContainer/AppContainer.tsx +++ b/GPTutor-Frontend/src/components/AppContainer/AppContainer.tsx @@ -47,7 +47,6 @@ function AppContainer({ const offsetHeightTabbar = tabbarElem?.offsetHeight || 0; const offsetHeightFixedBottom = fixedBottom?.offsetHeight || 0; - console.log(offsetHeightTabbar); const offset = offsetHeightHeader + offsetHeightTabbar + offsetHeightFixedBottom; diff --git a/GPTutor-Frontend/src/entity/routing/routing.ts b/GPTutor-Frontend/src/entity/routing/routing.ts index 063963dd..6fd719f9 100644 --- a/GPTutor-Frontend/src/entity/routing/routing.ts +++ b/GPTutor-Frontend/src/entity/routing/routing.ts @@ -41,6 +41,8 @@ export enum RoutingPages { vkDocQuestionPanel = "/vk-doc-question-panel", vkDocQuestionRequest = "/vk-doc-question-request", + + docQuestion = "/doc-question", } export enum Views { @@ -89,6 +91,8 @@ export enum Panels { vkDocQuestionPanel = "vk-doc-question-panel", vkDocQuestionRequest = "vk-doc-question-request", + + docQuestion = "doc-question", } export enum Modals { diff --git a/GPTutor-Frontend/src/env.js b/GPTutor-Frontend/src/env.js new file mode 100644 index 00000000..9b292a94 --- /dev/null +++ b/GPTutor-Frontend/src/env.js @@ -0,0 +1,100 @@ +window.env = { + USERDOMAIN_ROAMINGPROFILE: "DESKTOP-74V26CI", + PROCESSOR_LEVEL: "6", + NVM_SYMLINK: "C:\\Program Files\\nodejs", + SESSIONNAME: "Console", + ALLUSERSPROFILE: "C:\\ProgramData", + INTEL_DEV_REDIST: + "C:\\Program Files (x86)\\Common Files\\Intel\\Shared Libraries\\", + PROCESSOR_ARCHITECTURE: "AMD64", + MIC_LD_LIBRARY_PATH: + "C:\\Program Files (x86)\\Common Files\\Intel\\Shared Libraries\\compiler\\lib\\mic", + GATEWAY_VM_OPTIONS: + "C:\\Users\\grisha_blyat\\Downloads\\PhpStorm\\jetbra\\jetbra\\vmoptions\\gateway.vmoptions", + PSModulePath: + "C:\\Program Files\\WindowsPowerShell\\Modules;C:\\Windows\\system32\\WindowsPowerShell\\v1.0\\Modules", + SystemDrive: "C:", + RIDER_VM_OPTIONS: + "C:\\Users\\grisha_blyat\\Downloads\\PhpStorm\\jetbra\\jetbra\\vmoptions\\rider.vmoptions", + ACSvcPort: "17532", + USERNAME: "grisha_blyat", + DEVECOSTUDIO_VM_OPTIONS: + "C:\\Users\\grisha_blyat\\Downloads\\PhpStorm\\jetbra\\jetbra\\vmoptions\\devecostudio.vmoptions", + STUDIO_VM_OPTIONS: + "C:\\Users\\grisha_blyat\\Downloads\\PhpStorm\\jetbra\\jetbra\\vmoptions\\studio.vmoptions", + "ProgramFiles(x86)": "C:\\Program Files (x86)", + ACSetupSvcPort: "23210", + FPS_BROWSER_USER_PROFILE_STRING: "Default", + PATHEXT: ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC", + APPCODE_VM_OPTIONS: + "C:\\Users\\grisha_blyat\\Downloads\\PhpStorm\\jetbra\\jetbra\\vmoptions\\appcode.vmoptions", + DriverData: "C:\\Windows\\System32\\Drivers\\DriverData", + "PyCharm Community Edition": + "C:\\Program Files\\JetBrains\\PyCharm Community Edition 2023.3.2\\bin;", + ProgramData: "C:\\ProgramData", + DATASPELL_VM_OPTIONS: + "C:\\Users\\grisha_blyat\\Downloads\\PhpStorm\\jetbra\\jetbra\\vmoptions\\dataspell.vmoptions", + ProgramW6432: "C:\\Program Files", + HOMEPATH: "\\Users\\grisha_blyat", + ESET_OPTIONS: + " ", + PROCESSOR_IDENTIFIER: "Intel64 Family 6 Model 183 Stepping 1, GenuineIntel", + ProgramFiles: "C:\\Program Files", + PUBLIC: "C:\\Users\\Public", + windir: "C:\\Windows", + PhpStorm: "C:\\Program Files\\JetBrains\\PhpStorm 2024.2.3\\bin;", + ZES_ENABLE_SYSMAN: "1", + DATAGRIP_VM_OPTIONS: + "C:\\Users\\grisha_blyat\\Downloads\\PhpStorm\\jetbra\\jetbra\\vmoptions\\datagrip.vmoptions", + LOCALAPPDATA: "C:\\Users\\grisha_blyat\\AppData\\Local", + ChocolateyLastPathUpdate: "133399408909721675", + "IntelliJ IDEA": "C:\\Program Files\\JetBrains\\IntelliJ IDEA 2023.2\\bin;", + USERDOMAIN: "DESKTOP-74V26CI", + WEBSTORM_VM_OPTIONS: + "C:\\Users\\grisha_blyat\\Downloads\\PhpStorm\\jetbra\\jetbra\\vmoptions\\webstorm.vmoptions", + LOGONSERVER: "\\\\DESKTOP-74V26CI", + PYCHARM_VM_OPTIONS: + "C:\\Users\\grisha_blyat\\Downloads\\PhpStorm\\jetbra\\jetbra\\vmoptions\\pycharm.vmoptions", + FPS_BROWSER_APP_PROFILE_STRING: "Internet Explorer", + CLION_VM_OPTIONS: + "C:\\Users\\grisha_blyat\\Downloads\\PhpStorm\\jetbra\\jetbra\\vmoptions\\clion.vmoptions", + JETBRAINSCLIENT_VM_OPTIONS: + "C:\\Users\\grisha_blyat\\Downloads\\PhpStorm\\jetbra\\jetbra\\vmoptions\\jetbrainsclient.vmoptions", + WebStorm: "C:\\Program Files\\JetBrains\\WebStorm 2023.2\\bin;", + RlsSvcPort: "22112", + OMP_WAIT_POLICY: "PASSIVE", + GOLAND_VM_OPTIONS: + "C:\\Users\\grisha_blyat\\Downloads\\PhpStorm\\jetbra\\jetbra\\vmoptions\\goland.vmoptions", + OneDrive: "C:\\Users\\ГРИШАП БЛЯТЬ\\OneDrive", + APPDATA: "C:\\Users\\grisha_blyat\\AppData\\Roaming", + IDEA_VM_OPTIONS: + "C:\\Users\\grisha_blyat\\Downloads\\PhpStorm\\jetbra\\jetbra\\vmoptions\\idea.vmoptions", + KMP_BLOCKTIME: "0", + RUBYMINE_VM_OPTIONS: + "C:\\Users\\grisha_blyat\\Downloads\\PhpStorm\\jetbra\\jetbra\\vmoptions\\rubymine.vmoptions", + ChocolateyInstall: "C:\\ProgramData\\chocolatey", + JETBRAINS_CLIENT_VM_OPTIONS: + "C:\\Users\\grisha_blyat\\Downloads\\PhpStorm\\jetbra\\jetbra\\vmoptions\\jetbrains_client.vmoptions", + CommonProgramFiles: "C:\\Program Files\\Common Files", + Path: "C:\\Program Files (x86)\\Common Files\\Intel\\Shared Libraries\\redist\\intel64\\compiler;C:\\Program Files (x86)\\Common Files\\Oracle\\Java\\java8path;C:\\Program Files (x86)\\Common Files\\Oracle\\Java\\javapath;C:\\ffmpeg\\bin;C:\\ffmpeg\\bin;C:\\Program Files\\Microsoft\\jdk-17.0.8.7-hotspot\\bin;C:\\Windows\\system32;C:\\Windows;C:\\Windows\\System32\\Wbem;C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\;C:\\Windows\\System32\\OpenSSH\\;C:\\Program Files\\Git\\cmd;C:\\ProgramData\\chocolatey\\bin;C:\\Program Files (x86)\\NVIDIA Corporation\\PhysX\\Common;C:\\Program Files\\dotnet\\;C:\\Users\\grisha_blyat\\AppData\\Roaming\\nvm;C:\\Program Files\\nodejs;C:\\Program Files\\NVIDIA Corporation\\NVIDIA app\\NvDLISR;C:\\Users\\grisha_blyat\\AppData\\Local\\Programs\\Python\\Python310\\Scripts\\;C:\\Users\\grisha_blyat\\AppData\\Local\\Programs\\Python\\Python310\\;C:\\Users\\grisha_blyat\\AppData\\Local\\Programs\\Python\\Python312\\Scripts\\;C:\\Users\\grisha_blyat\\AppData\\Local\\Programs\\Python\\Python312\\;C:\\Users\\ГРИШАП БЛЯТЬ\\AppData\\Local\\Programs\\Python\\Python311\\Scripts\\;C:\\Users\\ГРИШАП БЛЯТЬ\\AppData\\Local\\Programs\\Python\\Python311\\;C:\\Users\\grisha_blyat\\AppData\\Local\\Microsoft\\WindowsAp;C:\\Program Files\\gs\\gs10.05.0\\bin;C:\\Program Files\\Docker\\Docker\\resources\\bin;C:\\Program Files\\cursor\\resources\\app\\bin;C:\\Users\\grisha_blyat\\AppData\\Local\\Programs\\Python\\Python310\\Scripts\\;C:\\Users\\grisha_blyat\\AppData\\Local\\Programs\\Python\\Python310\\;C:\\Users\\grisha_blyat\\AppData\\Local\\Programs\\Python\\Python312\\Scripts\\;C:\\Users\\grisha_blyat\\AppData\\Local\\Programs\\Python\\Python312\\;C:\\Users\\ГРИШАП БЛЯТЬ\\AppData\\Local\\Programs\\Python\\Python311\\Scripts\\;C:\\Users\\ГРИШАП БЛЯТЬ\\AppData\\Local\\Programs\\Python\\Python311\\;C:\\Users\\grisha_blyat\\AppData\\Local\\Microsoft\\WindowsApps;C:\\Users\\ГРИШАП БЛЯТЬ\\AppData\\Roaming\\npm;C:\\Program Files\\JetBrains\\WebStorm 2023.2\\bin;;C:\\Program Files\\JetBrains\\PyCharm 2023.2\\bin;;C:\\Program Files\\JetBrains\\IntelliJ IDEA 2023.2\\bin;;C:\\Program Files\\JetBrains\\PhpStorm 2024.2.3\\bin;;C:\\Program Files\\JetBrains\\PyCharm Community Edition 2023.3.2\\bin;;C:\\Users\\grisha_blyat\\AppData\\Local\\Programs\\Microsoft VS Code\\bin;C:\\Users\\grisha_blyat\\AppData\\Roaming\\npm;C:\\Users\\grisha_blyat\\AppData\\Roaming\\nvm;C:\\Program Files\\nodejs", + PyCharm: "C:\\Program Files\\JetBrains\\PyCharm 2023.2\\bin;", + OS: "Windows_NT", + COMPUTERNAME: "DESKTOP-74V26CI", + NVM_HOME: "C:\\Users\\grisha_blyat\\AppData\\Roaming\\nvm", + PROCESSOR_REVISION: "b701", + CommonProgramW6432: "C:\\Program Files\\Common Files", + ComSpec: "C:\\Windows\\system32\\cmd.exe", + TEMP: "C:\\Users\\GRISHA~1\\AppData\\Local\\Temp", + SystemRoot: "C:\\Windows", + WEBIDE_VM_OPTIONS: + "C:\\Users\\grisha_blyat\\Downloads\\PhpStorm\\jetbra\\jetbra\\vmoptions\\webide.vmoptions", + HOMEDRIVE: "C:", + USERPROFILE: "C:\\Users\\grisha_blyat", + TMP: "C:\\Users\\GRISHA~1\\AppData\\Local\\Temp", + "CommonProgramFiles(x86)": "C:\\Program Files (x86)\\Common Files", + NUMBER_OF_PROCESSORS: "24", + PHPSTORM_VM_OPTIONS: + "C:\\Users\\grisha_blyat\\Downloads\\PhpStorm\\jetbra\\jetbra\\vmoptions\\phpstorm.vmoptions", + REACT_APP: "API LLM", + REACT_APP_PLATFORM: "VK", +}; diff --git a/GPTutor-Frontend/src/index.tsx b/GPTutor-Frontend/src/index.tsx index 6da3fb41..16eac8e7 100644 --- a/GPTutor-Frontend/src/index.tsx +++ b/GPTutor-Frontend/src/index.tsx @@ -12,12 +12,10 @@ import { OnboardingService } from "./services/OnboardingService"; import { NavigationContextProvider } from "$/NavigationContext"; import { adService } from "$/services/AdService"; import { appService } from "$/services/AppService"; -import { subscriptionsController } from "$/entity/subscriptions"; import { VkStorageService } from "$/services/VkStorageService"; import "react-lazy-load-image-component/src/effects/black-and-white.css"; import { userInfo } from "$/entity/user/UserInfo"; import { listenResize } from "./resizeWindow"; -import { additionalRequests } from "$/entity/additionalRequest/AdditionalRequests"; import { platformAdapter } from "$/services/PlatformAdapterService"; const isFirstVisitFlagName = "isFirstVisit"; @@ -39,20 +37,20 @@ async function VKInit() { storageService.set(isFirstVisitFlagName, String(true)); }); - if (appService.isStableArt()) { - await userInfo.getUserImageAgreement(); - } + // if (appService.isStableArt()) { + // await userInfo.getUserImageAgreement(); + // } await adService.showBannerAd(); - if (appService.isGPTutor()) { - await subscriptionsController.getSubscription("subscription_2"); - } - await additionalRequests.init(); + // if (appService.isGPTutor()) { + // await subscriptionsController.getSubscription("subscription_2"); + // } + // await additionalRequests.init(); } platformAdapter .webAppInit() .then(async () => { - await userInfo.getUserBalance(); + // await userInfo.getUserBalance(); if (appService.isVK()) { await VKInit(); @@ -128,6 +126,7 @@ const routes = { Panels.vkDocQuestionRequest, Views.viewMain ), + [RoutingPages.docQuestion]: new Page(Panels.docQuestion, Views.viewMain), }; const router = new Router(routes); diff --git a/GPTutor-Frontend/src/panels/ApiLLMPanel/ApiLLMPanel.module.css b/GPTutor-Frontend/src/panels/ApiLLMPanel/ApiLLMPanel.module.css new file mode 100644 index 00000000..4befc648 --- /dev/null +++ b/GPTutor-Frontend/src/panels/ApiLLMPanel/ApiLLMPanel.module.css @@ -0,0 +1,370 @@ +.container { + width: 100vw; +} + +.containerPlaceholder { + width: 100vw; + height: 100%; +} + +.apiCode { + display: flex; + align-items: center; + gap: 6px; +} + +.copyIcon { + background: var(--vkui--color_background_modal); + color: var(--vkui--color_text_accent_themed); + border-radius: 8px; + transition: all 0.2s ease; +} + +.copyIcon:hover { + background: var(--vkui--color_background_accent_themed); + color: var(--vkui--color_text_contrast_themed); +} + +.cardApi { + width: 100%; +} + +.actions { + display: flex; + gap: 8px; +} + +.cardProfile { + display: grid; +} + +.cardName { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +} + +.subtitleText { + color: var(--vkui--color_text_secondary); +} + +/* Новые стили для API панели */ +.errorCard { + border-color: var(--vkui--color_stroke_negative); + background: var(--vkui--color_background_negative_tint); +} + +.errorHeader { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} + +.responseHeader { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; + justify-content: space-between; +} + +.responseText { + white-space: pre-wrap; + word-wrap: break-word; + line-height: 1.5; + padding: 12px; + background: var(--vkui--color_background_secondary); + border-radius: 8px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +.codeHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom: 1px solid var(--vkui--color_separator_primary); +} + +.codeBlock { + background: var(--vkui--color_background_secondary); + border-radius: 8px; + padding: 16px; + overflow-x: auto; + font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; + font-size: 13px; + line-height: 1.4; + margin: 0; + border: 1px solid var(--vkui--color_separator_primary); +} + +.codeBlock code { + color: var(--vkui--color_text_primary); + background: none; + padding: 0; + font-size: inherit; + font-family: inherit; +} + +/* Стили для табов */ +.tabContent { + padding-bottom: 80px; +} + +/* Улучшенные стили для слайдеров */ +.sliderContainer { + padding: 8px 0; +} + +.parameterDescription { + color: var(--vkui--color_text_secondary); + font-size: 14px; + margin-top: 4px; + line-height: 1.3; +} + +/* Стили для форм */ +.formGroup { + margin-bottom: 20px; +} + +.apiKeyInput { + font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; +} + +/* Анимации */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.fadeIn { + animation: fadeIn 0.3s ease-out; +} + +/* Адаптивность */ +@media (max-width: 768px) { + .codeBlock { + font-size: 12px; + padding: 12px; + } + + .responseText { + font-size: 14px; + padding: 10px; + } +} + +/* Улучшенные стили для карточек */ +.apiCard { + transition: all 0.2s ease; + border-radius: 12px; +} + +.apiCard:hover { + transform: translateY(-2px); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); +} + +/* Стили для статусных индикаторов */ +.statusSuccess { + color: var(--vkui--color_text_positive); +} + +.statusError { + color: var(--vkui--color_text_negative); +} + +.statusPending { + color: var(--vkui--color_text_secondary); +} + +/* Стили для баланса */ +.balanceCard { + background: linear-gradient(135deg, var(--vkui--color_background_accent_themed) 0%, var(--vkui--color_background_accent_tint_themed) 100%); + border: none; +} + +.balanceHeader { + display: flex; + align-items: center; + gap: 12px; +} + +.balanceSubtitle { + color: var(--vkui--color_text_secondary); + margin-top: 2px; +} + +.balanceActions { + display: flex; + flex-direction: column; + gap: 12px; +} + +.costEstimate { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: var(--vkui--color_background_warning_tint); + border-radius: 8px; + font-size: 13px; +} + +/* Стили для примеров */ +.examplesGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 12px; + margin-bottom: 16px; +} + +.exampleCard { + transition: all 0.2s ease; + cursor: pointer; +} + +.exampleCard:hover { + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); +} + +.exampleHeader { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} + +.exampleDescription { + color: var(--vkui--color_text_secondary); + font-size: 14px; + line-height: 1.4; +} + +/* Улучшенные стили для кода */ +.codeActions { + display: flex; + align-items: center; + gap: 8px; +} + +.downloadIcon { + background: var(--vkui--color_background_positive_tint); + color: var(--vkui--color_text_positive); + border-radius: 8px; + transition: all 0.2s ease; +} + +.downloadIcon:hover { + background: var(--vkui--color_background_positive_themed); + color: var(--vkui--color_text_contrast_themed); +} + +/* Улучшенный код блок */ +.codeBlock { + background: var(--vkui--color_background_secondary); + border-radius: 12px; + padding: 20px; + overflow-x: auto; + font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; + font-size: 13px; + line-height: 1.5; + margin: 0; + border: 1px solid var(--vkui--color_separator_primary); + position: relative; +} + +.codeBlock::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4); + border-radius: 12px 12px 0 0; +} + +/* Улучшенные стили для заголовков кода */ +.codeHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 2px solid var(--vkui--color_separator_primary); +} + +/* Стили для мобильных устройств */ +@media (max-width: 768px) { + .examplesGrid { + grid-template-columns: 1fr; + gap: 8px; + } + + .balanceHeader { + flex-direction: column; + align-items: flex-start; + gap: 8px; + } + + .codeActions { + flex-direction: column; + gap: 4px; + } +} + +/* Анимация для карточек */ +.fadeInUp { + animation: fadeInUp 0.4s ease-out; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Стили для статистики использования */ +.usageStats { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: var(--vkui--color_background_secondary); + border-radius: 8px; + margin-top: 12px; +} + +.statItem { + text-align: center; +} + +.statValue { + font-weight: 600; + font-size: 16px; + color: var(--vkui--color_text_primary); +} + +.statLabel { + font-size: 12px; + color: var(--vkui--color_text_secondary); + margin-top: 2px; +} diff --git a/GPTutor-Frontend/src/panels/ApiLLMPanel/index.tsx b/GPTutor-Frontend/src/panels/ApiLLMPanel/index.tsx new file mode 100644 index 00000000..3323f50c --- /dev/null +++ b/GPTutor-Frontend/src/panels/ApiLLMPanel/index.tsx @@ -0,0 +1,801 @@ +import * as React from "react"; +import { + Button, + Card, + CardGrid, + Div, + FormItem, + FormLayoutGroup, + Input, + Textarea, + Slider, + Select, + Checkbox, + IconButton, + Panel, + PanelHeader, + PanelHeaderBack, + Spacing, + Title, + Caption, + Headline, + Text, + Separator, + Group, + Header, + SimpleCell, + Switch, + FixedLayout, + Tabbar, + TabbarItem, +} from "@vkontakte/vkui"; +import { AppContainer } from "$/components/AppContainer"; +import { useNavigationContext } from "$/NavigationContext"; +import PanelTitle from "$/components/PanelTitle"; +import { CardBlock } from "$/components/CardBlock"; +import { + Icon20CopyOutline, + Icon28DocumentOutline, + Icon24ReplayOutline, + Icon28SettingsOutline, + Icon24QuestionOutline, + Icon24KeyOutline, + Icon24BrainOutline, + Icon20CheckCircleFillGreen, + Icon24ErrorCircleFillRed, + Icon24MoneyCircleOutline, + Icon24AddCircleOutline, + Icon24LightbulbOutline, + Icon24InfoCircleOutline, + Icon24DollarCircleOutline, + Icon24PlayCircle, +} from "@vkontakte/icons"; + +import classes from "./ApiLLMPanel.module.css"; +import TertiaryTitle from "$/components/TertiaryTitle"; +import { AppDiv } from "$/components/AppDiv"; + +interface IProps { + id: string; +} + +interface OpenAIParams { + apiKey: string; + model: string; + temperature: number; + maxTokens: number; + topP: number; + frequencyPenalty: number; + presencePenalty: number; + systemPrompt: string; + userMessage: string; + stream: boolean; +} + +interface UserBalance { + balance: number; + currency: string; +} + +interface PromptExample { + title: string; + systemPrompt: string; + userMessage: string; + description: string; +} + +function ApiLLMPanel({ id }: IProps) { + const { goBack } = useNavigationContext(); + const [activeTab, setActiveTab] = React.useState("settings"); + const [isLoading, setIsLoading] = React.useState(false); + const [response, setResponse] = React.useState(""); + const [error, setError] = React.useState(""); + const [userBalance, setUserBalance] = React.useState({ + balance: 15.5, + currency: "USD", + }); + const [estimatedCost, setEstimatedCost] = React.useState(0); + + const [params, setParams] = React.useState({ + apiKey: "", + model: "gpt-3.5-turbo", + temperature: 0.7, + maxTokens: 1000, + topP: 1, + frequencyPenalty: 0, + presencePenalty: 0, + systemPrompt: "You are a helpful assistant.", + userMessage: "", + stream: false, + }); + + const models = [ + { label: "GPT-3.5 Turbo", value: "gpt-3.5-turbo" }, + { label: "GPT-4", value: "gpt-4" }, + { label: "GPT-4 Turbo", value: "gpt-4-turbo-preview" }, + { label: "GPT-4o", value: "gpt-4o" }, + { label: "GPT-4o Mini", value: "gpt-4o-mini" }, + ]; + + const promptExamples: PromptExample[] = [ + { + title: "Программист-помощник", + description: "Помощь с написанием и отладкой кода", + systemPrompt: + "Ты опытный программист. Помогай писать чистый, эффективный код и объясняй сложные концепции простым языком.", + userMessage: "Напиши функцию на Python для сортировки массива пузырьком", + }, + { + title: "Переводчик", + description: "Профессиональный перевод текстов", + systemPrompt: + "Ты профессиональный переводчик. Переводи тексты точно, сохраняя смысл и стиль оригинала.", + userMessage: + "Переведи на английский: 'Привет, как дела? Надеюсь, у тебя все хорошо.'", + }, + { + title: "Аналитик данных", + description: "Анализ и интерпретация данных", + systemPrompt: + "Ты эксперт по анализу данных. Помогай интерпретировать данные, находить закономерности и делать выводы.", + userMessage: "Объясни, что такое корреляция и как её интерпретировать", + }, + { + title: "Творческий писатель", + description: "Создание креативного контента", + systemPrompt: + "Ты творческий писатель с богатым воображением. Создавай интересные истории, стихи и креативный контент.", + userMessage: + "Напиши короткий рассказ о роботе, который учится быть человеком", + }, + ]; + + const modelPricing = { + "gpt-3.5-turbo": { input: 0.0015, output: 0.002 }, + "gpt-4": { input: 0.03, output: 0.06 }, + "gpt-4-turbo-preview": { input: 0.01, output: 0.03 }, + "gpt-4o": { input: 0.005, output: 0.015 }, + "gpt-4o-mini": { input: 0.00015, output: 0.0006 }, + }; + + const handleParamChange = (key: keyof OpenAIParams, value: any) => { + setParams((prev) => ({ ...prev, [key]: value })); + if ( + key === "model" || + key === "maxTokens" || + key === "userMessage" || + key === "systemPrompt" + ) { + calculateEstimatedCost({ ...params, [key]: value }); + } + }; + + const calculateEstimatedCost = (currentParams: OpenAIParams) => { + const pricing = + modelPricing[currentParams.model as keyof typeof modelPricing]; + if (!pricing) return; + + // Примерная оценка токенов (1 токен ≈ 4 символа) + const inputTokens = Math.ceil( + (currentParams.systemPrompt.length + currentParams.userMessage.length) / 4 + ); + const outputTokens = currentParams.maxTokens; + + const inputCost = (inputTokens / 1000) * pricing.input; + const outputCost = (outputTokens / 1000) * pricing.output; + const totalCost = inputCost + outputCost; + + setEstimatedCost(totalCost); + }; + + const applyPromptExample = (example: PromptExample) => { + setParams((prev) => ({ + ...prev, + systemPrompt: example.systemPrompt, + userMessage: example.userMessage, + })); + calculateEstimatedCost({ + ...params, + systemPrompt: example.systemPrompt, + userMessage: example.userMessage, + }); + }; + + const handleTopUpBalance = () => { + // Здесь будет логика пополнения баланса + alert("Функция пополнения баланса будет реализована позже"); + }; + + React.useEffect(() => { + calculateEstimatedCost(params); + }, [params.model, params.maxTokens, params.userMessage, params.systemPrompt]); + + const handleTestAPI = async () => { + if (!params.apiKey.trim()) { + setError("API ключ обязателен"); + return; + } + if (!params.userMessage.trim()) { + setError("Сообщение пользователя обязательно"); + return; + } + + setIsLoading(true); + setError(""); + setResponse(""); + + try { + const requestBody = { + model: params.model, + messages: [ + { role: "system", content: params.systemPrompt }, + { role: "user", content: params.userMessage }, + ], + temperature: params.temperature, + max_tokens: params.maxTokens, + top_p: params.topP, + frequency_penalty: params.frequencyPenalty, + presence_penalty: params.presencePenalty, + stream: params.stream, + }; + + const response = await fetch( + "https://api.openai.com/v1/chat/completions", + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${params.apiKey}`, + }, + body: JSON.stringify(requestBody), + } + ); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error?.message || "Ошибка API"); + } + + setResponse(data.choices[0].message.content); + } catch (err: any) { + setError(err.message || "Произошла ошибка"); + } finally { + setIsLoading(false); + } + }; + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + }; + + const executeCode = (codeType: "python" | "javascript" | "curl") => { + let code = ""; + switch (codeType) { + case "python": + code = generatePythonCode(); + break; + case "javascript": + code = generateJavaScriptCode(); + break; + case "curl": + code = generateCurlCode(); + break; + } + + const blob = new Blob([code], { type: "text/plain" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `openai_example.${ + codeType === "curl" ? "sh" : codeType === "python" ? "py" : "js" + }`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + const generatePythonCode = () => { + return `import openai +import json + +# Настройка API ключа +openai.api_key = "${params.apiKey || "YOUR_API_KEY"}" + +# Параметры запроса +response = openai.ChatCompletion.create( + model="${params.model}", + messages=[ + {"role": "system", "content": "${params.systemPrompt}"}, + {"role": "user", "content": "${params.userMessage || "Ваше сообщение"}"} + ], + temperature=${params.temperature}, + max_tokens=${params.maxTokens}, + top_p=${params.topP}, + frequency_penalty=${params.frequencyPenalty}, + presence_penalty=${params.presencePenalty}, + stream=${params.stream ? "True" : "False"} +) + +print(json.dumps(response, indent=2, ensure_ascii=False))`; + }; + + const generateJavaScriptCode = () => { + return `const OpenAI = require('openai'); + +const openai = new OpenAI({ + apiKey: '${params.apiKey || "YOUR_API_KEY"}', +}); + +async function main() { + const completion = await openai.chat.completions.create({ + messages: [ + { role: 'system', content: '${params.systemPrompt}' }, + { role: 'user', content: '${params.userMessage || "Ваше сообщение"}' } + ], + model: '${params.model}', + temperature: ${params.temperature}, + max_tokens: ${params.maxTokens}, + top_p: ${params.topP}, + frequency_penalty: ${params.frequencyPenalty}, + presence_penalty: ${params.presencePenalty}, + stream: ${params.stream} + }); + + console.log(completion.choices[0].message.content); +} + +main();`; + }; + + const generateCurlCode = () => { + const requestBody = { + model: params.model, + messages: [ + { role: "system", content: params.systemPrompt }, + { role: "user", content: params.userMessage || "Ваше сообщение" }, + ], + temperature: params.temperature, + max_tokens: params.maxTokens, + top_p: params.topP, + frequency_penalty: params.frequencyPenalty, + presence_penalty: params.presencePenalty, + stream: params.stream, + }; + + return `curl https://api.openai.com/v1/chat/completions \\ + -H "Content-Type: application/json" \\ + -H "Authorization: Bearer ${params.apiKey || "YOUR_API_KEY"}" \\ + -d '${JSON.stringify(requestBody, null, 2)}'`; + }; + + return ( + + }> + + + } + childrenWithHeight={(height) => ( +
+ + + setActiveTab("settings")} + selected={activeTab === "settings"} + text="Настройки" + > + + + setActiveTab("test")} + selected={activeTab === "test"} + text="Тест API" + > + + + setActiveTab("code")} + selected={activeTab === "code"} + text="Примеры кода" + > + + + + + +
+ {activeTab === "settings" && ( + +
Баланс пользователя
+ +
+
+ +
+ + ${userBalance.balance.toFixed(2)}{" "} + {userBalance.currency} + + + Доступный баланс + +
+
+ +
+ + {estimatedCost > 0 && ( +
+ + + Примерная стоимость запроса: $ + {estimatedCost.toFixed(6)} + +
+ )} +
+
+
+ + + +
API Конфигурация
+ + + + handleParamChange("apiKey", e.target.value) + } + before={} + /> + + + +