commit d5ed7fdcf97152f70997ec78f3a4bbb3c87cb7d3 Author: laruevin Date: Wed Feb 11 11:42:42 2026 +0700 Initial commit: Guidly project with CI/CD pipeline Telegram Bot + Mini App for city walking quests. - React 19 + TypeScript + Vite 6 frontend - Express 5 + PostgreSQL backend - grammY Telegram bot with DeepSeek AI - GitLab CI/CD: lint, build, deploy to production Co-Authored-By: Claude Opus 4.6 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..14aef1f --- /dev/null +++ b/.env.example @@ -0,0 +1,25 @@ +# PostgreSQL +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=guidly +DB_USER=guidly +DB_PASSWORD= +DATABASE_SSL=false + +# Telegram Bot +TELEGRAM_BOT_TOKEN= +BOT_WEBHOOK_URL=https://guidly.example.com/api/telegram/webhook +BOT_WEBHOOK_SECRET= + +# DeepSeek LLM (OpenAI-compatible API) +LLM_BASE_URL=https://api.deepseek.com +LLM_API_KEY= +LLM_MODEL=deepseek-chat + +# Internal API (shared secret between bot and API server) +INTERNAL_API_KEY= + +# Application +NODE_ENV=development +PORT=3000 +APP_URL=http://localhost:5173 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7ac73e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# Dependencies +node_modules/ + +# Build output +dist/ +dist-server/ +dist-bot/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Env files +.env +.env.local +.env.*.local + +# Logs +*.log +npm-debug.log* + +# Temp files +~$* + +# Docs +*.docx +*.xlsx + +# Deploy secrets & artifacts +deploy/.env.production +deploy/.env.* +deploy.tar.gz + +# Local files +.claude/ +Инструкция.txt diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..9229334 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,70 @@ +stages: + - lint + - build + - deploy + +# Шаблон: Node.js 22 + кэш node_modules +.node_setup: + image: node:22-alpine + before_script: + - npm ci --prefer-offline + cache: + key: + files: + - package-lock.json + paths: + - node_modules/ + +# Lint — на каждый push и MR +lint: + extends: .node_setup + stage: lint + script: + - npm run lint + +# Build — компиляция TS + Vite сборка +build: + extends: .node_setup + stage: build + script: + - npm run build + artifacts: + paths: + - dist/ + - dist-server/ + - dist-bot/ + expire_in: 1 hour + +# Deploy — только при push в main +deploy_production: + stage: deploy + image: node:22-alpine + dependencies: + - build + before_script: + - apk add --no-cache openssh-client + - eval $(ssh-agent -s) + - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - + - mkdir -p ~/.ssh + - echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts + - chmod 644 ~/.ssh/known_hosts + script: + - tar -czf deploy.tar.gz dist/ dist-server/ dist-bot/ package.json package-lock.json ecosystem.config.cjs deploy/nginx-guidly.conf + - scp deploy.tar.gz "${DEPLOY_USER}@${DEPLOY_HOST}:/tmp/" + - | + ssh "${DEPLOY_USER}@${DEPLOY_HOST}" << 'ENDSSH' + set -e + cd /root/guidly + tar -xzf /tmp/deploy.tar.gz + rm /tmp/deploy.tar.gz + npm install --omit=dev + pm2 delete guidly-server 2>/dev/null || true + pm2 start ecosystem.config.cjs + pm2 save + echo "=== Deploy complete ===" + ENDSSH + rules: + - if: $CI_COMMIT_BRANCH == "main" + environment: + name: production + url: https://gidli.ru diff --git a/api/_lib/auth.ts b/api/_lib/auth.ts new file mode 100644 index 0000000..d61eb95 --- /dev/null +++ b/api/_lib/auth.ts @@ -0,0 +1,116 @@ +import crypto from 'crypto' +import type { ApiRequest, ApiResponse } from './types.js' + +const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || '' +const INTERNAL_API_KEY = process.env.INTERNAL_API_KEY || 'dev-internal-key' + +const ALLOWED_ORIGINS = [ + 'https://web.telegram.org', + process.env.APP_URL || null, + process.env.NODE_ENV === 'development' ? 'http://localhost:5173' : null, +].filter(Boolean) as string[] + +// Validate Telegram Mini App initData +// https://core.telegram.org/bots/webapps#validating-data-received-via-the-mini-app +export function validateTelegramInitData(initData: string): { id: number; username?: string; first_name?: string; last_name?: string; language_code?: string } | null { + if (!initData || !TELEGRAM_BOT_TOKEN) return null + + try { + const params = new URLSearchParams(initData) + const hash = params.get('hash') + if (!hash) return null + + params.delete('hash') + + // Sort and create data-check-string + const dataCheckString = Array.from(params.entries()) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, value]) => `${key}=${value}`) + .join('\n') + + // Create secret key + const secretKey = crypto + .createHmac('sha256', 'WebAppData') + .update(TELEGRAM_BOT_TOKEN) + .digest() + + // Compute HMAC + const computedHash = crypto + .createHmac('sha256', secretKey) + .update(dataCheckString) + .digest('hex') + + if (computedHash !== hash) return null + + // Check auth_date is not too old (24 hours) + const authDate = params.get('auth_date') + if (authDate) { + const authTimestamp = parseInt(authDate) * 1000 + const now = Date.now() + if (now - authTimestamp > 24 * 60 * 60 * 1000) return null + } + + // Parse user data + const userStr = params.get('user') + if (!userStr) return null + + return JSON.parse(userStr) + } catch { + return null + } +} + +// Middleware: authenticate via Telegram initData +export function withTelegramAuth(handler: Function) { + return async (req: ApiRequest, res: ApiResponse) => { + const initData = req.headers['x-telegram-init-data'] as string + const user = validateTelegramInitData(initData) + + if (!user) { + // In development, allow bypass with query param + if (process.env.NODE_ENV === 'development' && req.query.telegram_id) { + req.telegramUser = { + id: parseInt(req.query.telegram_id as string), + first_name: 'Dev', + language_code: 'ru', + } + return handler(req, res) + } + + return res.status(401).json({ error: 'Invalid Telegram authentication' }) + } + + req.telegramUser = user + return handler(req, res) + } +} + +// Middleware: authenticate internal bot-to-API calls +export function withInternalAuth(handler: Function) { + return async (req: ApiRequest, res: ApiResponse) => { + const apiKey = req.headers['x-internal-api-key'] as string + + if (apiKey !== INTERNAL_API_KEY) { + return res.status(401).json({ error: 'Invalid internal API key' }) + } + + return handler(req, res) + } +} + +// CORS handler +export function cors(req: ApiRequest, res: ApiResponse) { + const origin = req.headers.origin + + if (origin && ALLOWED_ORIGINS.includes(origin)) { + res.setHeader('Access-Control-Allow-Origin', origin) + } else if (process.env.NODE_ENV === 'development' && origin) { + if (origin.startsWith('http://localhost:') || origin.startsWith('http://127.0.0.1:')) { + res.setHeader('Access-Control-Allow-Origin', origin) + } + } + + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Telegram-Init-Data, X-Internal-Api-Key') + res.setHeader('Access-Control-Allow-Credentials', 'true') +} diff --git a/api/_lib/db.ts b/api/_lib/db.ts new file mode 100644 index 0000000..5659702 --- /dev/null +++ b/api/_lib/db.ts @@ -0,0 +1,368 @@ +import { Pool } from 'pg' + +const pool = new Pool({ + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT || '5432'), + database: process.env.DB_NAME, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + ssl: process.env.DATABASE_SSL === 'false' ? false : { rejectUnauthorized: false }, +}) + +// ============================================ +// Users +// ============================================ + +export async function findUserByTelegramId(telegramId: number) { + const { rows } = await pool.query( + 'SELECT * FROM users WHERE telegram_id = $1', + [telegramId] + ) + return rows[0] || null +} + +export async function createUser(data: { + telegram_id: number + username?: string + display_name?: string + language_code?: string +}) { + const { rows } = await pool.query( + `INSERT INTO users (telegram_id, username, display_name, language_code) + VALUES ($1, $2, $3, $4) + ON CONFLICT (telegram_id) DO UPDATE SET + username = COALESCE($2, users.username), + display_name = COALESCE($3, users.display_name), + language_code = COALESCE($4, users.language_code), + updated_at = NOW() + RETURNING *`, + [data.telegram_id, data.username || null, data.display_name || null, data.language_code || 'ru'] + ) + return rows[0] +} + +// ============================================ +// Cities +// ============================================ + +export async function getActiveCities() { + const { rows } = await pool.query( + "SELECT * FROM cities WHERE status = 'active' ORDER BY name" + ) + return rows +} + +export async function getCityById(id: string) { + const { rows } = await pool.query('SELECT * FROM cities WHERE id = $1', [id]) + return rows[0] || null +} + +// ============================================ +// Quests +// ============================================ + +export async function getActiveQuestForUser(userId: string) { + const { rows } = await pool.query( + `SELECT q.*, c.name as city_name, c.country as city_country + FROM quests q + JOIN cities c ON q.city_id = c.id + WHERE q.user_id = $1 AND q.status = 'in_progress' + LIMIT 1`, + [userId] + ) + return rows[0] || null +} + +export async function createQuest(data: { + city_id: string + user_id: string + title?: string + description?: string + number_of_days: number + pace: string + companions: string + user_comment?: string +}) { + const { rows } = await pool.query( + `INSERT INTO quests (city_id, user_id, title, description, number_of_days, pace, companions, user_comment) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING *`, + [ + data.city_id, data.user_id, data.title || null, data.description || null, + data.number_of_days, data.pace, data.companions, data.user_comment || null + ] + ) + return rows[0] +} + +export async function updateQuestStatus(questId: string, status: string) { + const extra = status === 'in_progress' + ? ", started_at = NOW()" + : status === 'completed' + ? ", completed_at = NOW()" + : "" + + const { rows } = await pool.query( + `UPDATE quests SET status = $1, updated_at = NOW()${extra} WHERE id = $2 RETURNING *`, + [status, questId] + ) + return rows[0] || null +} + +// ============================================ +// Quest Days +// ============================================ + +export async function createQuestDay(data: { + quest_id: string + day_number: number + theme?: string +}) { + const { rows } = await pool.query( + `INSERT INTO quest_days (quest_id, day_number, theme) + VALUES ($1, $2, $3) + RETURNING *`, + [data.quest_id, data.day_number, data.theme || null] + ) + return rows[0] +} + +export async function getQuestDays(questId: string) { + const { rows } = await pool.query( + 'SELECT * FROM quest_days WHERE quest_id = $1 ORDER BY day_number', + [questId] + ) + return rows +} + +export async function getActiveDayForQuest(questId: string) { + const { rows } = await pool.query( + `SELECT * FROM quest_days + WHERE quest_id = $1 AND status = 'in_progress' + LIMIT 1`, + [questId] + ) + return rows[0] || null +} + +export async function updateDayStatus(dayId: string, status: string) { + const extra = status === 'in_progress' + ? ", started_at = NOW()" + : status === 'completed' + ? ", completed_at = NOW()" + : "" + + const { rows } = await pool.query( + `UPDATE quest_days SET status = $1${extra} WHERE id = $2 RETURNING *`, + [status, dayId] + ) + return rows[0] || null +} + +// ============================================ +// Quest Points +// ============================================ + +export async function createQuestPoint(data: { + day_id: string + title: string + location_lat?: number + location_lon?: number + teaser_text?: string + content_text?: string + order_in_day: number + status?: string +}) { + const { rows } = await pool.query( + `INSERT INTO quest_points (day_id, title, location_lat, location_lon, teaser_text, content_text, order_in_day, status) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + ON CONFLICT (day_id, order_in_day) DO NOTHING + RETURNING *`, + [ + data.day_id, data.title, data.location_lat || null, data.location_lon || null, + data.teaser_text || null, data.content_text || null, data.order_in_day, + data.status || 'hidden' + ] + ) + return rows[0] || null +} + +export async function getActivePointForDay(dayId: string) { + const { rows } = await pool.query( + `SELECT * FROM quest_points + WHERE day_id = $1 AND status = 'active' + LIMIT 1`, + [dayId] + ) + return rows[0] || null +} + +export async function getPointsForDay(dayId: string) { + const { rows } = await pool.query( + 'SELECT * FROM quest_points WHERE day_id = $1 ORDER BY order_in_day', + [dayId] + ) + return rows +} + +export async function getPointById(pointId: string) { + const { rows } = await pool.query( + 'SELECT * FROM quest_points WHERE id = $1', + [pointId] + ) + return rows[0] || null +} + +export async function updatePointStatus(pointId: string, status: string) { + const extra = status === 'active' + ? "" + : status === 'completed' + ? ", completed_at = NOW()" + : "" + + const arrivedExtra = status === 'active' ? "" : "" + + const { rows } = await pool.query( + `UPDATE quest_points SET status = $1${extra} WHERE id = $2 RETURNING *`, + [status, pointId] + ) + return rows[0] || null +} + +export async function markPointArrived(pointId: string) { + const { rows } = await pool.query( + `UPDATE quest_points SET arrived_at = NOW() WHERE id = $1 RETURNING *`, + [pointId] + ) + return rows[0] || null +} + +export async function updatePointContent(pointId: string, contentText: string) { + const { rows } = await pool.query( + `UPDATE quest_points SET content_text = $1 WHERE id = $2 RETURNING *`, + [contentText, pointId] + ) + return rows[0] || null +} + +// ============================================ +// Quest Events +// ============================================ + +export async function createQuestEvent(data: { + quest_id: string + user_id: string + point_id?: string + event_type: string + payload?: Record +}) { + const { rows } = await pool.query( + `INSERT INTO quest_events (quest_id, user_id, point_id, event_type, payload) + VALUES ($1, $2, $3, $4, $5) + RETURNING *`, + [data.quest_id, data.user_id, data.point_id || null, data.event_type, JSON.stringify(data.payload || {})] + ) + return rows[0] +} + +export async function getUnprocessedEvents() { + const { rows } = await pool.query( + `SELECT * FROM quest_events + WHERE processed = false + ORDER BY created_at ASC + LIMIT 20` + ) + return rows +} + +export async function markEventProcessed(eventId: string) { + await pool.query( + 'UPDATE quest_events SET processed = true WHERE id = $1', + [eventId] + ) +} + +// ============================================ +// Chat History +// ============================================ + +export async function saveChatMessage(data: { + user_id: string + quest_id: string + point_id?: string + message_type: string + content: string +}) { + const { rows } = await pool.query( + `INSERT INTO chat_history (user_id, quest_id, point_id, message_type, content) + VALUES ($1, $2, $3, $4, $5) + RETURNING *`, + [data.user_id, data.quest_id, data.point_id || null, data.message_type, data.content] + ) + return rows[0] +} + +export async function getChatHistory(questId: string) { + const { rows } = await pool.query( + 'SELECT * FROM chat_history WHERE quest_id = $1 ORDER BY created_at', + [questId] + ) + return rows +} + +// ============================================ +// Achievements +// ============================================ + +export async function createAchievement(data: { + user_id: string + type: string + city_id?: string +}) { + const { rows } = await pool.query( + `INSERT INTO achievements (user_id, type, city_id) + VALUES ($1, $2, $3) + ON CONFLICT (user_id, type, city_id) DO NOTHING + RETURNING *`, + [data.user_id, data.type, data.city_id || null] + ) + return rows[0] || null +} + +export async function getUserAchievements(userId: string) { + const { rows } = await pool.query( + 'SELECT * FROM achievements WHERE user_id = $1 ORDER BY achieved_at', + [userId] + ) + return rows +} + +// ============================================ +// Full quest state (for API responses) +// ============================================ + +export async function getFullQuestState(userId: string) { + const quest = await getActiveQuestForUser(userId) + if (!quest) return null + + const day = await getActiveDayForQuest(quest.id) + if (!day) { + return { quest, day: null, point: null, dayProgress: null } + } + + const points = await getPointsForDay(day.id) + const activePoint = points.find((p: { status: string }) => p.status === 'active') || null + const completedCount = points.filter((p: { status: string }) => p.status === 'completed').length + + return { + quest, + day, + point: activePoint, + dayProgress: { + totalPoints: points.length, + completedPoints: completedCount, + } + } +} + +export { pool } diff --git a/api/_lib/types.ts b/api/_lib/types.ts new file mode 100644 index 0000000..36330d6 --- /dev/null +++ b/api/_lib/types.ts @@ -0,0 +1,16 @@ +import type { Request, Response } from 'express' + +export interface ApiRequest extends Request { + query: Record + telegramUser?: { + id: number + username?: string + first_name?: string + last_name?: string + language_code?: string + } +} + +export interface ApiResponse extends Response {} + +export type ApiHandler = (req: ApiRequest, res: ApiResponse) => Promise | void | ApiResponse diff --git a/api/_lib/validation.ts b/api/_lib/validation.ts new file mode 100644 index 0000000..4817350 --- /dev/null +++ b/api/_lib/validation.ts @@ -0,0 +1,23 @@ +import { z } from 'zod' + +export const questEventSchema = z.object({ + questId: z.string().uuid(), + pointId: z.string().uuid().optional(), + eventType: z.enum([ + 'arrived', 'photo_uploaded', 'skipped', + 'finished_today', 'point_completed', + 'quest_started', 'quest_completed', + 'day_started', 'day_completed', + 'peek_next' + ]), + payload: z.record(z.unknown()).optional(), +}) + +export const internalActionSchema = z.object({ + action: z.enum([ + 'create_quest', 'generate_step', 'update_point_content', + 'update_state', 'get_user_state', 'save_chat_message', + 'check_achievements' + ]), + data: z.record(z.unknown()), +}) diff --git a/api/bot/internal.ts b/api/bot/internal.ts new file mode 100644 index 0000000..cfa571b --- /dev/null +++ b/api/bot/internal.ts @@ -0,0 +1,127 @@ +import type { ApiRequest, ApiResponse } from '../_lib/types.js' +import { withInternalAuth } from '../_lib/auth.js' +import { + createQuest, createQuestDay, createQuestPoint, + updateQuestStatus, updateDayStatus, updatePointStatus, + updatePointContent, markPointArrived, + getActiveQuestForUser, getActiveDayForQuest, + getPointsForDay, getFullQuestState, + saveChatMessage, createAchievement, getUserAchievements, + findUserByTelegramId, +} from '../_lib/db.js' + +async function handler(req: ApiRequest, res: ApiResponse) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }) + } + + const { action, data } = req.body + + try { + switch (action) { + case 'create_quest': { + const quest = await createQuest({ + city_id: data.city_id, + user_id: data.user_id, + title: data.title, + description: data.description, + number_of_days: data.number_of_days, + pace: data.pace, + companions: data.companions, + user_comment: data.user_comment, + }) + + // Create day skeletons + const days = [] + for (let i = 1; i <= data.number_of_days; i++) { + const day = await createQuestDay({ + quest_id: quest.id, + day_number: i, + theme: data.day_themes?.[i - 1] || null, + }) + days.push(day) + } + + return res.json({ quest, days }) + } + + case 'generate_step': { + const point = await createQuestPoint({ + day_id: data.day_id, + title: data.title, + location_lat: data.location_lat, + location_lon: data.location_lon, + teaser_text: data.teaser_text, + order_in_day: data.order_in_day, + status: data.status || 'active', + }) + return res.json({ point }) + } + + case 'update_point_content': { + const point = await updatePointContent(data.point_id, data.content_text) + return res.json({ point }) + } + + case 'update_state': { + let result = null + if (data.entity === 'quest') { + result = await updateQuestStatus(data.id, data.status) + } else if (data.entity === 'day') { + result = await updateDayStatus(data.id, data.status) + } else if (data.entity === 'point') { + result = await updatePointStatus(data.id, data.status) + if (data.status === 'active' && data.arrived) { + await markPointArrived(data.id) + } + } + return res.json({ result }) + } + + case 'get_user_state': { + const user = await findUserByTelegramId(data.telegram_id) + if (!user) { + return res.json({ state: null }) + } + const state = await getFullQuestState(user.id) + return res.json({ state }) + } + + case 'save_chat_message': { + const message = await saveChatMessage({ + user_id: data.user_id, + quest_id: data.quest_id, + point_id: data.point_id, + message_type: data.message_type, + content: data.content, + }) + return res.json({ message }) + } + + case 'check_achievements': { + const existing = await getUserAchievements(data.user_id) + const newAchievements = [] + + // First quest completed + if (!existing.find((a: any) => a.type === 'first_quest')) { + const ach = await createAchievement({ + user_id: data.user_id, + type: 'first_quest', + city_id: data.city_id, + }) + if (ach) newAchievements.push(ach) + } + + return res.json({ achievements: newAchievements }) + } + + default: + return res.status(400).json({ error: `Unknown action: ${action}` }) + } + } catch (error: any) { + console.error(`Internal API error (${action}):`, error) + return res.status(500).json({ error: error.message }) + } +} + +export default withInternalAuth(handler) diff --git a/api/quest/current.ts b/api/quest/current.ts new file mode 100644 index 0000000..7173de4 --- /dev/null +++ b/api/quest/current.ts @@ -0,0 +1,34 @@ +import type { ApiRequest, ApiResponse } from '../_lib/types.js' +import { cors, withTelegramAuth } from '../_lib/auth.js' +import { findUserByTelegramId, getFullQuestState } from '../_lib/db.js' + +async function handler(req: ApiRequest, res: ApiResponse) { + cors(req, res) + + if (req.method === 'OPTIONS') { + return res.status(200).end() + } + + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }) + } + + const telegramUser = req.telegramUser + if (!telegramUser) { + return res.status(401).json({ error: 'Not authenticated' }) + } + + const user = await findUserByTelegramId(telegramUser.id) + if (!user) { + return res.status(404).json({ error: 'User not found', code: 'USER_NOT_FOUND' }) + } + + const state = await getFullQuestState(user.id) + if (!state) { + return res.status(404).json({ error: 'No active quest', code: 'NO_ACTIVE_QUEST' }) + } + + return res.json(state) +} + +export default withTelegramAuth(handler) diff --git a/api/quest/events.ts b/api/quest/events.ts new file mode 100644 index 0000000..8dd7031 --- /dev/null +++ b/api/quest/events.ts @@ -0,0 +1,122 @@ +import type { ApiRequest, ApiResponse } from '../_lib/types.js' +import { cors, withTelegramAuth } from '../_lib/auth.js' +import { questEventSchema } from '../_lib/validation.js' +import { + findUserByTelegramId, + createQuestEvent, + getPointById, + updatePointStatus, + markPointArrived, + updatePointContent, + getActivePointForDay, + getActiveDayForQuest, + getActiveQuestForUser, + updateDayStatus, + getPointsForDay, +} from '../_lib/db.js' + +async function handler(req: ApiRequest, res: ApiResponse) { + cors(req, res) + + if (req.method === 'OPTIONS') { + return res.status(200).end() + } + + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }) + } + + const telegramUser = req.telegramUser + if (!telegramUser) { + return res.status(401).json({ error: 'Not authenticated' }) + } + + const parsed = questEventSchema.safeParse(req.body) + if (!parsed.success) { + return res.status(400).json({ error: 'Invalid request', details: parsed.error.issues }) + } + + const { questId, pointId, eventType, payload } = parsed.data + + const user = await findUserByTelegramId(telegramUser.id) + if (!user) { + return res.status(404).json({ error: 'User not found' }) + } + + // Write event to DB (bot will process it asynchronously) + await createQuestEvent({ + quest_id: questId, + user_id: user.id, + point_id: pointId, + event_type: eventType, + payload, + }) + + // Immediate state updates for UI responsiveness + let nextAction: string = 'wait' + let updatedPoint = null + + switch (eventType) { + case 'arrived': { + if (pointId) { + await markPointArrived(pointId) + updatedPoint = await getPointById(pointId) + nextAction = 'show_content' + } + break + } + case 'point_completed': { + if (pointId) { + await updatePointStatus(pointId, 'completed') + // Check if there are more points in the day + const quest = await getActiveQuestForUser(user.id) + if (quest) { + const day = await getActiveDayForQuest(quest.id) + if (day) { + const points = await getPointsForDay(day.id) + const nextPoint = points.find((p: { status: string }) => p.status === 'hidden' || p.status === 'preview') + if (nextPoint) { + nextAction = 'next_point' + } else { + nextAction = 'day_complete' + } + } + } + } + break + } + case 'skipped': { + if (pointId) { + await updatePointStatus(pointId, 'skipped') + nextAction = 'next_point' + } + break + } + case 'finished_today': { + const quest = await getActiveQuestForUser(user.id) + if (quest) { + const day = await getActiveDayForQuest(quest.id) + if (day) { + // Mark all remaining points as skipped + const points = await getPointsForDay(day.id) + for (const point of points) { + if (point.status !== 'completed' && point.status !== 'skipped') { + await updatePointStatus(point.id, 'skipped') + } + } + await updateDayStatus(day.id, 'completed') + nextAction = 'day_complete' + } + } + break + } + } + + return res.json({ + success: true, + updatedPoint, + nextAction, + }) +} + +export default withTelegramAuth(handler) diff --git a/api/quest/history.ts b/api/quest/history.ts new file mode 100644 index 0000000..f8aa4fc --- /dev/null +++ b/api/quest/history.ts @@ -0,0 +1,79 @@ +import type { ApiRequest, ApiResponse } from '../_lib/types.js' +import { cors, withTelegramAuth } from '../_lib/auth.js' +import { findUserByTelegramId, pool } from '../_lib/db.js' + +async function handler(req: ApiRequest, res: ApiResponse) { + cors(req, res) + + if (req.method === 'OPTIONS') { + return res.status(200).end() + } + + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }) + } + + const telegramUser = req.telegramUser + if (!telegramUser) { + return res.status(401).json({ error: 'Not authenticated' }) + } + + const user = await findUserByTelegramId(telegramUser.id) + if (!user) { + return res.status(404).json({ error: 'User not found' }) + } + + const questId = req.query.id as string | undefined + + if (questId) { + // Get specific quest with days and points + const { rows: quests } = await pool.query( + `SELECT q.*, c.name as city_name, c.country as city_country + FROM quests q + JOIN cities c ON q.city_id = c.id + WHERE q.id = $1 AND q.user_id = $2`, + [questId, user.id] + ) + + if (quests.length === 0) { + return res.status(404).json({ error: 'Quest not found' }) + } + + const { rows: days } = await pool.query( + `SELECT d.*, + COALESCE( + json_agg( + jsonb_build_object( + 'id', p.id, 'title', p.title, 'teaser_text', p.teaser_text, + 'content_text', p.content_text, 'status', p.status, + 'order_in_day', p.order_in_day, 'location_lat', p.location_lat, + 'location_lon', p.location_lon + ) ORDER BY p.order_in_day + ) FILTER (WHERE p.id IS NOT NULL), + '[]'::json + ) as points + FROM quest_days d + LEFT JOIN quest_points p ON d.id = p.day_id + WHERE d.quest_id = $1 + GROUP BY d.id + ORDER BY d.day_number`, + [questId] + ) + + return res.json({ quest: quests[0], days }) + } + + // List all completed quests + const { rows: quests } = await pool.query( + `SELECT q.*, c.name as city_name, c.country as city_country + FROM quests q + JOIN cities c ON q.city_id = c.id + WHERE q.user_id = $1 AND q.status = 'completed' + ORDER BY q.completed_at DESC`, + [user.id] + ) + + return res.json({ quests }) +} + +export default withTelegramAuth(handler) diff --git a/api/telegram/webhook.ts b/api/telegram/webhook.ts new file mode 100644 index 0000000..2bfa9a9 --- /dev/null +++ b/api/telegram/webhook.ts @@ -0,0 +1,24 @@ +import type { ApiRequest, ApiResponse } from '../_lib/types.js' + +// Telegram webhook endpoint +// In production, the bot process handles updates via grammY's built-in webhook support +// This endpoint serves as a passthrough for webhook mode + +async function handler(req: ApiRequest, res: ApiResponse) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }) + } + + // Verify webhook secret + const secret = req.headers['x-telegram-bot-api-secret-token'] + if (secret !== process.env.BOT_WEBHOOK_SECRET) { + return res.status(401).json({ error: 'Invalid secret' }) + } + + // The bot process will handle updates directly in webhook mode + // This is a placeholder for when we integrate webhook into the server + console.log('Received Telegram webhook update') + return res.status(200).json({ ok: true }) +} + +export default handler diff --git a/bot/bot.ts b/bot/bot.ts new file mode 100644 index 0000000..ffd3bfe --- /dev/null +++ b/bot/bot.ts @@ -0,0 +1,171 @@ +import { Bot, session } from 'grammy' +import { conversations, createConversation } from '@grammyjs/conversations' +import type { BotContext, SessionData } from './types.js' +import { onboardingConversation } from './conversations/onboarding.js' +import { createUser, findUserByTelegramId, getActiveQuestForUser } from '../api/_lib/db.js' +import { t, getLang } from './i18n/index.js' + +// Helper: build reply_markup for opening the Mini App +// Telegram requires HTTPS for both web_app and url buttons. +// In dev (HTTP), we skip the button entirely and append the link to the message text. +function questMarkup(label: string): { inline_keyboard: any[][] } | undefined { + const appUrl = process.env.APP_URL || 'https://guidly.example.com' + if (appUrl.startsWith('https://')) { + return { inline_keyboard: [[ { text: label, web_app: { url: appUrl } } ]] } + } + return undefined +} + +function appendAppLink(text: string): string { + const appUrl = process.env.APP_URL || 'https://guidly.example.com' + if (!appUrl.startsWith('https://')) { + return text + `\n\n🔗 ${appUrl}` + } + return text +} + +export function createBot(token: string) { + const bot = new Bot(token) + + // Error handler — log but don't crash + bot.catch((err) => { + console.error('Bot error:', err.message) + }) + + // Session middleware (in-memory for now, can switch to DB later) + bot.use(session({ + initial: (): SessionData => ({ + conversationState: 'idle', + }), + })) + + // Conversations plugin + bot.use(conversations()) + bot.use(createConversation(onboardingConversation)) + + // /start command + bot.command('start', async (ctx) => { + const from = ctx.from + if (!from) return + + const lang = getLang(from.language_code) + + // Upsert user in DB + const user = await createUser({ + telegram_id: from.id, + username: from.username, + display_name: [from.first_name, from.last_name].filter(Boolean).join(' '), + language_code: from.language_code || 'ru', + }) + + ctx.session.userId = user.id + + // Check if user has an active quest + const activeQuest = await getActiveQuestForUser(user.id) + + if (activeQuest) { + // Resume active quest + await ctx.reply(appendAppLink(t(lang, 'welcome_back', { city: activeQuest.city_name })), { + reply_markup: questMarkup(t(lang, 'open_quest')), + }) + return + } + + // New user or no active quest — start onboarding + await ctx.reply(t(lang, 'welcome')) + await ctx.conversation.enter('onboardingConversation') + }) + + // /help command + bot.command('help', async (ctx) => { + const lang = getLang(ctx.from?.language_code) + await ctx.reply(t(lang, 'help')) + }) + + // /status command + bot.command('status', async (ctx) => { + const from = ctx.from + if (!from) return + + const lang = getLang(from.language_code) + const user = await findUserByTelegramId(from.id) + if (!user) { + await ctx.reply(t(lang, 'no_profile')) + return + } + + const quest = await getActiveQuestForUser(user.id) + if (!quest) { + await ctx.reply(t(lang, 'no_active_quest')) + return + } + + await ctx.reply(t(lang, 'quest_status', { + title: quest.title || quest.city_name, + city: quest.city_name, + status: quest.status, + })) + }) + + // /stop command — terminate quest early + bot.command('stop', async (ctx) => { + const lang = getLang(ctx.from?.language_code) + await ctx.reply(t(lang, 'stop_confirm'), { + reply_markup: { + inline_keyboard: [ + [ + { text: t(lang, 'yes_stop'), callback_data: 'quest_stop_confirm' }, + { text: t(lang, 'no_continue'), callback_data: 'quest_stop_cancel' }, + ], + ], + }, + }) + }) + + // Callback: quest stop confirm + bot.callbackQuery('quest_stop_confirm', async (ctx) => { + const from = ctx.from + const lang = getLang(from.language_code) + const user = await findUserByTelegramId(from.id) + + if (user) { + const quest = await getActiveQuestForUser(user.id) + if (quest) { + // Import dynamically to avoid circular deps + const { updateQuestStatus } = await import('../api/_lib/db.js') + await updateQuestStatus(quest.id, 'completed') + await ctx.editMessageText(t(lang, 'quest_stopped')) + return + } + } + + await ctx.editMessageText(t(lang, 'no_active_quest')) + }) + + bot.callbackQuery('quest_stop_cancel', async (ctx) => { + const lang = getLang(ctx.from.language_code) + await ctx.editMessageText(t(lang, 'quest_continue')) + }) + + // Handle any text when no conversation is active + bot.on('message:text', async (ctx) => { + const lang = getLang(ctx.from?.language_code) + const user = await findUserByTelegramId(ctx.from.id) + + if (!user) { + await ctx.reply(t(lang, 'use_start')) + return + } + + const quest = await getActiveQuestForUser(user.id) + if (quest) { + await ctx.reply(appendAppLink(t(lang, 'quest_in_progress')), { + reply_markup: questMarkup(t(lang, 'open_quest')), + }) + } else { + await ctx.reply(t(lang, 'no_active_quest_start')) + } + }) + + return bot +} diff --git a/bot/conversations/onboarding.ts b/bot/conversations/onboarding.ts new file mode 100644 index 0000000..f25e6c6 --- /dev/null +++ b/bot/conversations/onboarding.ts @@ -0,0 +1,191 @@ +import type { Conversation } from '@grammyjs/conversations' +import type { BotContext } from '../types.js' + +type BotConversation = Conversation +import { getActiveCities, createUser, findUserByTelegramId } from '../../api/_lib/db.js' +import { t, getLang } from '../i18n/index.js' +import { createQuestFromOnboarding } from '../services/quest.service.js' + +const COMPANIONS_MAP: Record = { + solo: 'solo', + couple: 'couple', + family: 'family', + friends: 'friends', +} + +const PACE_MAP: Record = { + slow: 'slow', + normal: 'normal', + active: 'active', +} + +export async function onboardingConversation(conversation: BotConversation, ctx: BotContext) { + const lang = getLang(ctx.from?.language_code) + + // Step 1: City selection + const cities = await conversation.external(() => getActiveCities()) + + const cityButtons = cities.map((city: any) => ([ + { text: city.name, callback_data: `city_${city.id}` } + ])) + + await ctx.reply(t(lang, 'choose_city'), { + reply_markup: { inline_keyboard: cityButtons }, + }) + + const cityResponse = await conversation.waitForCallbackQuery(/^city_/) + const cityId = cityResponse.callbackQuery!.data!.replace('city_', '') + const cityName = cities.find((c: any) => c.id === cityId)?.name || 'Unknown' + await cityResponse.answerCallbackQuery() + + // Step 2: Number of days + await ctx.reply(t(lang, 'choose_days')) + + let days: number = 0 + while (true) { + const daysResponse = await conversation.waitFor('message:text') + const parsed = parseInt(daysResponse.message.text) + if (parsed >= 1 && parsed <= 30) { + days = parsed + break + } + await ctx.reply(t(lang, 'invalid_days')) + } + + // Step 3: Companions + await ctx.reply(t(lang, 'choose_companions'), { + reply_markup: { + inline_keyboard: [ + [ + { text: t(lang, 'companions_solo'), callback_data: 'comp_solo' }, + { text: t(lang, 'companions_couple'), callback_data: 'comp_couple' }, + ], + [ + { text: t(lang, 'companions_family'), callback_data: 'comp_family' }, + { text: t(lang, 'companions_friends'), callback_data: 'comp_friends' }, + ], + ], + }, + }) + + const compResponse = await conversation.waitForCallbackQuery(/^comp_/) + const companions = COMPANIONS_MAP[compResponse.callbackQuery!.data!.replace('comp_', '')] || 'solo' + await compResponse.answerCallbackQuery() + + // Step 4: Pace + await ctx.reply(t(lang, 'choose_pace'), { + reply_markup: { + inline_keyboard: [ + [{ text: t(lang, 'pace_slow'), callback_data: 'pace_slow' }], + [{ text: t(lang, 'pace_normal'), callback_data: 'pace_normal' }], + [{ text: t(lang, 'pace_active'), callback_data: 'pace_active' }], + ], + }, + }) + + const paceResponse = await conversation.waitForCallbackQuery(/^pace_/) + const pace = PACE_MAP[paceResponse.callbackQuery!.data!.replace('pace_', '')] || 'normal' + await paceResponse.answerCallbackQuery() + + // Step 5: Wishes (optional) + await ctx.reply(t(lang, 'add_wishes'), { + reply_markup: { + inline_keyboard: [ + [{ text: t(lang, 'skip'), callback_data: 'wishes_skip' }], + ], + }, + }) + + let userComment: string | undefined + + // Wait for either text or callback + const wishesCtx = await conversation.wait() + + if (wishesCtx.callbackQuery?.data === 'wishes_skip') { + await wishesCtx.answerCallbackQuery() + userComment = undefined + } else if (wishesCtx.message?.text) { + userComment = wishesCtx.message.text + } + + // Step 6: Confirmation + const companionsLabel = t(lang, `companions_${companions}`) + const paceLabel = t(lang, `pace_${pace}`) + const wishesLine = userComment ? t(lang, 'wishes_line', { comment: userComment }) : '' + + await ctx.reply( + t(lang, 'onboarding_summary', { + city: cityName, + days: days, + companions: companionsLabel, + pace: paceLabel, + wishes: wishesLine, + }), + { + reply_markup: { + inline_keyboard: [ + [ + { text: t(lang, 'confirm_yes'), callback_data: 'onboarding_confirm' }, + { text: t(lang, 'confirm_restart'), callback_data: 'onboarding_restart' }, + ], + ], + }, + } + ) + + const confirmResponse = await conversation.waitForCallbackQuery(/^onboarding_/) + await confirmResponse.answerCallbackQuery() + + if (confirmResponse.callbackQuery!.data === 'onboarding_restart') { + // Restart the conversation + await ctx.reply(t(lang, 'welcome')) + return + } + + // Step 7: Create quest + await ctx.reply(t(lang, 'creating_quest')) + + const userId = await conversation.external(async () => { + const user = await findUserByTelegramId(ctx.from!.id) + return user?.id + }) + + if (!userId) { + await ctx.reply(t(lang, 'use_start')) + return + } + + const result = await conversation.external(() => + createQuestFromOnboarding({ + userId, + cityId, + cityName, + days, + companions, + pace, + userComment, + lang, + }) + ) + + if (!result) { + await ctx.reply('Something went wrong. Please try /start again.') + return + } + + const appUrl = process.env.APP_URL || 'https://guidly.example.com' + const isHttps = appUrl.startsWith('https://') + const questText = t(lang, 'quest_created', { + title: result.title, + description: result.description, + }) + (isHttps ? '' : `\n\n🔗 ${appUrl}`) + + await ctx.reply( + questText, + isHttps ? { + reply_markup: { + inline_keyboard: [[ { text: t(lang, 'open_quest'), web_app: { url: appUrl } } ]], + }, + } : {} + ) +} diff --git a/bot/i18n/en.ts b/bot/i18n/en.ts new file mode 100644 index 0000000..0738380 --- /dev/null +++ b/bot/i18n/en.ts @@ -0,0 +1,78 @@ +export const en: Record = { + // Welcome & general + welcome: `Hi! I'm Guidly — your personal city guide and adventure companion. + +I'll turn your walk into a real adventure — step by step, like a quest. + +Let's begin! Where are you headed?`, + + welcome_back: `Welcome back! You have an active quest in {city}. Open the app to continue.`, + + // Onboarding + choose_city: 'Choose a city for your adventure:', + choose_days: 'How many days are you planning? (1-30)', + invalid_days: 'Please enter a number from 1 to 30.', + choose_companions: 'Who are you traveling with?', + companions_solo: 'Solo', + companions_couple: 'As a couple', + companions_family: 'With family', + companions_friends: 'With friends', + choose_pace: 'What pace do you prefer?', + pace_slow: 'Relaxed — no rush, enjoying every moment', + pace_normal: 'Moderate — a nice balance', + pace_active: 'Active — I want to see as much as possible', + add_wishes: 'Any special wishes? What interests you most? (or tap "Skip")', + skip: 'Skip', + onboarding_summary: `Great! Here's your plan: + +City: {city} +Days: {days} +Company: {companions} +Pace: {pace} +{wishes} +All good?`, + wishes_line: 'Wishes: {comment}', + confirm_yes: "Yes, let's go!", + confirm_restart: 'Start over', + + // Quest creation + creating_quest: "Awesome! Creating your quest... Please wait a moment.", + quest_created: `Your quest "{title}" is ready! + +{description} + +Open the app to see your first step.`, + + // Commands + help: `Available commands: +/start — start or continue a quest +/status — current progress +/stop — end quest early +/help — show this help`, + + // Status + no_profile: 'Tap /start to begin.', + no_active_quest: "You don't have an active quest. Tap /start to create one.", + no_active_quest_start: "You don't have an active quest. Tap /start to create a new one.", + quest_status: 'Quest: {title}\nCity: {city}\nStatus: in progress', + quest_in_progress: 'You have an active quest! Open the app to continue.', + use_start: 'Tap /start to begin.', + + // Stop + stop_confirm: 'Are you sure you want to end the quest early? Your progress will be saved.', + yes_stop: 'Yes, end it', + no_continue: 'No, continue', + quest_stopped: 'Quest ended. You can start a new one with /start.', + quest_continue: "Great! Let's keep going.", + + // Quest buttons + open_quest: 'Open Quest', + + // Event reactions + reaction_arrived: "You're here! Here's what awaits you...", + reaction_skipped: "No worries, let's move on!", + reaction_finished_today: 'Great day! Rest up, we continue tomorrow.', + reaction_point_completed: "Excellent! Let's head to the next spot...", + reaction_day_completed: "Day complete! Well done. See you tomorrow!", + reaction_quest_completed: "Congratulations! Quest complete! You've discovered the city in a whole new way.", +} diff --git a/bot/i18n/index.ts b/bot/i18n/index.ts new file mode 100644 index 0000000..19a6e69 --- /dev/null +++ b/bot/i18n/index.ts @@ -0,0 +1,25 @@ +import { ru } from './ru.js' +import { en } from './en.js' + +const translations: Record> = { ru, en } + +export function getLang(languageCode?: string): string { + if (!languageCode) return 'ru' + if (languageCode.startsWith('ru') || languageCode.startsWith('uk') || languageCode.startsWith('be')) { + return 'ru' + } + return 'en' +} + +export function t(lang: string, key: string, params?: Record): string { + const dict = translations[lang] || translations['ru'] + let text = dict[key] || translations['ru'][key] || key + + if (params) { + for (const [k, v] of Object.entries(params)) { + text = text.replace(`{${k}}`, String(v)) + } + } + + return text +} diff --git a/bot/i18n/ru.ts b/bot/i18n/ru.ts new file mode 100644 index 0000000..14cebff --- /dev/null +++ b/bot/i18n/ru.ts @@ -0,0 +1,78 @@ +export const ru: Record = { + // Welcome & general + welcome: `Привет! Я Guidly — твой персональный гид-проводник по городам мира. + +Я превращу твою прогулку в настоящее приключение — шаг за шагом, как квест. + +Давай начнём! Расскажи, куда ты собираешься?`, + + welcome_back: `С возвращением! У тебя есть активный квест по городу {city}. Открой приложение, чтобы продолжить.`, + + // Onboarding + choose_city: 'Выбери город для приключения:', + choose_days: 'На сколько дней планируешь поездку? (1-30)', + invalid_days: 'Введи число от 1 до 30.', + choose_companions: 'С кем путешествуешь?', + companions_solo: 'Один', + companions_couple: 'Вдвоём', + companions_family: 'С семьёй', + companions_friends: 'С друзьями', + choose_pace: 'В каком темпе хочешь гулять?', + pace_slow: 'Спокойный — без спешки, наслаждаюсь', + pace_normal: 'Умеренный — золотая середина', + pace_active: 'Активный — хочу увидеть максимум', + add_wishes: 'Есть пожелания? Что тебе особенно интересно? (или нажми "Пропустить")', + skip: 'Пропустить', + onboarding_summary: `Отлично! Вот твой план: + +Город: {city} +Дней: {days} +Компания: {companions} +Темп: {pace} +{wishes} +Всё верно?`, + wishes_line: 'Пожелания: {comment}', + confirm_yes: 'Да, начинаем!', + confirm_restart: 'Начать сначала', + + // Quest creation + creating_quest: 'Отлично! Создаю твой квест... Подожди немного.', + quest_created: `Твой квест "{title}" готов! + +{description} + +Открой приложение, чтобы увидеть первый шаг.`, + + // Commands + help: `Доступные команды: +/start — начать или продолжить квест +/status — текущий прогресс +/stop — завершить квест досрочно +/help — показать эту справку`, + + // Status + no_profile: 'Нажми /start, чтобы начать.', + no_active_quest: 'У тебя нет активного квеста. Нажми /start, чтобы создать.', + no_active_quest_start: 'У тебя нет активного квеста. Нажми /start, чтобы создать новый.', + quest_status: 'Квест: {title}\nГород: {city}\nСтатус: в процессе', + quest_in_progress: 'У тебя есть активный квест! Открой приложение, чтобы продолжить.', + use_start: 'Нажми /start, чтобы начать.', + + // Stop + stop_confirm: 'Ты уверен, что хочешь завершить квест досрочно? Прогресс будет сохранён.', + yes_stop: 'Да, завершить', + no_continue: 'Нет, продолжить', + quest_stopped: 'Квест завершён. Ты можешь начать новый квест с помощью /start.', + quest_continue: 'Отлично! Продолжаем приключение.', + + // Quest buttons + open_quest: 'Открыть квест', + + // Event reactions + reaction_arrived: 'Ты на месте! Вот что тебя здесь ждёт...', + reaction_skipped: 'Ничего страшного, пропускаем. Идём дальше!', + reaction_finished_today: 'Хороший день! Отдыхай, завтра продолжим.', + reaction_point_completed: 'Отлично! Идём к следующей точке...', + reaction_day_completed: 'День завершён! Ты молодец. До завтра!', + reaction_quest_completed: 'Поздравляю! Квест пройден! Ты открыл для себя город по-новому.', +} diff --git a/bot/index.ts b/bot/index.ts new file mode 100644 index 0000000..3b05472 --- /dev/null +++ b/bot/index.ts @@ -0,0 +1,42 @@ +import 'dotenv/config' +import { createBot } from './bot.js' +import { startEventProcessor } from './services/event-processor.js' + +const token = process.env.TELEGRAM_BOT_TOKEN + +if (!token) { + console.error('TELEGRAM_BOT_TOKEN is not set!') + process.exit(1) +} + +const bot = createBot(token) + +// Start event processing loop (polls quest_events table) +startEventProcessor(bot) + +// Start bot +if (process.env.NODE_ENV === 'production' && process.env.BOT_WEBHOOK_URL) { + // Webhook mode for production + bot.api.setWebhook(process.env.BOT_WEBHOOK_URL, { + secret_token: process.env.BOT_WEBHOOK_SECRET, + }) + console.log('Guidly bot started in webhook mode') +} else { + // Polling mode for development + bot.start({ + onStart: () => { + console.log('Guidly bot started in polling mode') + }, + }) +} + +// Graceful shutdown +process.on('SIGTERM', () => { + console.log('Shutting down bot...') + bot.stop() +}) + +process.on('SIGINT', () => { + console.log('Shutting down bot...') + bot.stop() +}) diff --git a/bot/prompts/point-content.ts b/bot/prompts/point-content.ts new file mode 100644 index 0000000..3e77c9b --- /dev/null +++ b/bot/prompts/point-content.ts @@ -0,0 +1,41 @@ +export function getPointContentPrompt(params: { + city: string + pointTitle: string + dayNumber: number + companions: string + pace: string + lang: string +}): string { + const langInstruction = params.lang === 'ru' + ? 'Write the content entirely in Russian.' + : 'Write the content entirely in English.' + + const toneMap: Record = { + solo: params.lang === 'ru' ? 'задумчивый, философский' : 'introspective, philosophical', + couple: params.lang === 'ru' ? 'романтический, с нотками приватности' : 'romantic, intimate', + family: params.lang === 'ru' ? 'познавательный, увлекательный' : 'educational, engaging', + friends: params.lang === 'ru' ? 'весёлый, энергичный' : 'fun, energetic', + } + + return `The traveler has arrived at: ${params.pointTitle} +City: ${params.city}, Day ${params.dayNumber} +Companions: ${params.companions}, Pace: ${params.pace} + +Generate a FULL guide content for this location. +This is the moment of revelation — the traveler is standing here right now. + +${langInstruction} + +Content must include: +- Opening that acknowledges their arrival (e.g., "You're standing in front of...") +- Historical context (2-3 paragraphs) +- Cultural significance +- Interesting facts and hidden details +- What to look for right now +- A personal tip or recommendation + +Length: 300-500 words +Tone: atmospheric, discovery-oriented, ${toneMap[params.companions] || 'balanced'} + +Do NOT include JSON formatting. Write natural flowing text with paragraphs.` +} diff --git a/bot/prompts/reaction.ts b/bot/prompts/reaction.ts new file mode 100644 index 0000000..4bf2e75 --- /dev/null +++ b/bot/prompts/reaction.ts @@ -0,0 +1,36 @@ +export function getReactionPrompt(params: { + eventType: string + pointTitle?: string + city: string + lang: string + companions: string + pace: string +}): string { + const langInstruction = params.lang === 'ru' + ? 'Write the reaction in Russian.' + : 'Write the reaction in English.' + + const eventDescriptions: Record = { + arrived: `The traveler has arrived at "${params.pointTitle}" in ${params.city}.`, + skipped: `The traveler decided to skip "${params.pointTitle}".`, + finished_today: `The traveler decided to finish for today in ${params.city}.`, + point_completed: `The traveler has finished exploring "${params.pointTitle}" and is ready for the next step.`, + day_completed: `The traveler has completed all points for today in ${params.city}.`, + quest_completed: `The traveler has completed the entire quest in ${params.city}!`, + } + + return `${eventDescriptions[params.eventType] || 'The traveler performed an action.'} + +Companions: ${params.companions}, Pace: ${params.pace} + +${langInstruction} + +Generate a short, atmospheric reaction message (2-3 sentences max). +- Keep the adventurous guide tone +- Be encouraging and warm +- If they skipped, be understanding +- If they completed, celebrate +- Make it personal and contextual + +Write only the message text, no formatting, no JSON.` +} diff --git a/bot/prompts/step-generation.ts b/bot/prompts/step-generation.ts new file mode 100644 index 0000000..9217aed --- /dev/null +++ b/bot/prompts/step-generation.ts @@ -0,0 +1,48 @@ +export function getStepGenerationPrompt(params: { + city: string + dayNumber: number + totalDays: number + dayTheme: string + pace: string + companions: string + userComment?: string + previousPoints: string[] + lang: string +}): string { + const langInstruction = params.lang === 'ru' + ? 'Write title and teaserText in Russian.' + : 'Write title and teaserText in English.' + + return `Given quest context: +- City: ${params.city} +- Day: ${params.dayNumber} of ${params.totalDays} +- Day theme: ${params.dayTheme} +- Pace: ${params.pace} +- Companions: ${params.companions} +${params.userComment ? `- User wishes: ${params.userComment}` : ''} +- Points already visited today: ${params.previousPoints.length > 0 ? params.previousPoints.join(', ') : 'none (this is the first point)'} + +Generate the NEXT point for today's route. + +${langInstruction} + +Return ONLY a valid JSON object: +{ + "title": "Location name", + "teaserText": "1-2 sentences teasing what awaits, creating curiosity", + "locationLat": , + "locationLon": , + "isLastPointOfDay": +} + +Requirements: +- Must be a REAL location in ${params.city} with accurate coordinates +- Must be geographically close to the previous point (walkable) +- Must not repeat any visited location +- Teaser should create curiosity without spoiling +- Consider the narrative arc of the day +- For ${params.pace} pace: ${{slow: '4-5 points per day', normal: '5-7 points per day', active: '7-9 points per day'}[params.pace] || '5-7 points per day'} +- Set isLastPointOfDay=true when it makes sense to end the day + +Return ONLY valid JSON, no markdown, no extra text.` +} diff --git a/bot/prompts/system.ts b/bot/prompts/system.ts new file mode 100644 index 0000000..373bed7 --- /dev/null +++ b/bot/prompts/system.ts @@ -0,0 +1,57 @@ +export function getSystemPrompt(lang: string): string { + if (lang === 'ru') { + return `Ты — Guidly, опытный и увлечённый городской гид. Ты ведёшь путешественников по городам шаг за шагом, создавая ощущение открытия и чуда. + +Твой стиль: +- Атмосферный и приключенческий, как друг, который знает город изнутри +- Тёплый, но без лишнего восторга +- Ты словно раскрываешь секреты города +- Никогда не выдаёшь всю информацию сразу — интригуешь, раскрываешь, удивляешь + +Адаптируй стиль: +- Один: более задумчивый, философский тон +- Вдвоём: романтический, с нотками приватности +- С семьёй: познавательный, увлекательный для всех возрастов +- С друзьями: весёлый, энергичный + +Темп: +- Спокойный: подробный, поэтичный +- Умеренный: сбалансированный +- Активный: энергичный, лаконичный + +ВАЖНЫЕ ПРАВИЛА: +- Никогда не раскрывай весь маршрут +- Генерируй только СЛЕДУЮЩИЙ шаг, никогда не несколько сразу +- После подтверждения прибытия — давай полный контент о локации +- Жди подтверждения перед генерацией следующего шага +- Поддерживай сюжетную связность между шагами +- Все ответы на русском языке` + } + + return `You are Guidly, an experienced and passionate city guide. You lead travelers through cities step by step, creating a sense of discovery and wonder. + +Your style: +- Atmospheric and adventurous, like a friend who knows the city inside out +- Warm but not overly enthusiastic +- You speak as if revealing the city's secrets +- Never dump all information at once — tease, reveal, and surprise + +Adapt your style based on companions: +- Solo: more introspective, philosophical tone +- Couple: romantic, with a sense of intimacy +- Family: educational, engaging for all ages +- Friends: fun, energetic + +Pace: +- Relaxed: detailed, poetic +- Moderate: balanced +- Active: energetic, concise + +CRITICAL RULES: +- Never reveal the full route +- Generate only the NEXT step, never multiple steps at once +- After arrival confirmation, provide rich, full content about the location +- Wait for confirmation before generating the next step +- Maintain narrative continuity across steps +- All responses in English` +} diff --git a/bot/services/ai.service.ts b/bot/services/ai.service.ts new file mode 100644 index 0000000..6daca78 --- /dev/null +++ b/bot/services/ai.service.ts @@ -0,0 +1,179 @@ +import OpenAI from 'openai' +import type { QuestPlan, GeneratedPoint, PointContent } from '../types.js' +import { getSystemPrompt } from '../prompts/system.js' +import { getStepGenerationPrompt } from '../prompts/step-generation.js' +import { getPointContentPrompt } from '../prompts/point-content.js' +import { getReactionPrompt } from '../prompts/reaction.js' + +const client = new OpenAI({ + baseURL: process.env.LLM_BASE_URL || 'https://api.deepseek.com', + apiKey: process.env.LLM_API_KEY || '', +}) + +const MODEL = process.env.LLM_MODEL || 'deepseek-chat' + +class AIService { + async generateQuestPlan(params: { + city: string + days: number + companions: string + pace: string + userComment?: string + lang: string + }): Promise { + const systemPrompt = getSystemPrompt(params.lang) + + const userPrompt = `Create a quest plan for a traveler: +- City: ${params.city} +- Duration: ${params.days} day(s) +- Companions: ${params.companions} +- Pace: ${params.pace} +${params.userComment ? `- Special wishes: ${params.userComment}` : ''} + +Return a JSON object with: +{ + "title": "Creative quest title (in ${params.lang === 'ru' ? 'Russian' : 'English'})", + "description": "1-2 sentence quest description that sets the mood (in ${params.lang === 'ru' ? 'Russian' : 'English'})", + "dayThemes": ["Theme for day 1", "Theme for day 2", ...] +} + +IMPORTANT: Return ONLY valid JSON, no markdown, no extra text.` + + const response = await client.chat.completions.create({ + model: MODEL, + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: userPrompt }, + ], + temperature: 0.8, + max_tokens: 1000, + }) + + const text = response.choices[0]?.message?.content?.trim() || '{}' + + try { + // Try to extract JSON from potential markdown code blocks + const jsonMatch = text.match(/\{[\s\S]*\}/) + const parsed = JSON.parse(jsonMatch ? jsonMatch[0] : text) + return { + title: parsed.title || `Adventure in ${params.city}`, + description: parsed.description || 'Your personal city quest awaits!', + dayThemes: parsed.dayThemes || Array(params.days).fill('Exploration'), + } + } catch { + console.error('Failed to parse quest plan:', text) + return { + title: params.lang === 'ru' + ? `Приключение в городе ${params.city}` + : `Adventure in ${params.city}`, + description: params.lang === 'ru' + ? 'Твой персональный городской квест ждёт!' + : 'Your personal city quest awaits!', + dayThemes: Array(params.days).fill( + params.lang === 'ru' ? 'Исследование' : 'Exploration' + ), + } + } + } + + async generatePoint(params: { + city: string + dayNumber: number + totalDays: number + dayTheme: string + pace: string + companions: string + userComment?: string + previousPoints: string[] + lang: string + }): Promise { + const systemPrompt = getSystemPrompt(params.lang) + const userPrompt = getStepGenerationPrompt(params) + + const response = await client.chat.completions.create({ + model: MODEL, + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: userPrompt }, + ], + temperature: 0.8, + max_tokens: 500, + }) + + const text = response.choices[0]?.message?.content?.trim() || '{}' + + try { + const jsonMatch = text.match(/\{[\s\S]*\}/) + const parsed = JSON.parse(jsonMatch ? jsonMatch[0] : text) + return { + title: parsed.title || 'Unknown Location', + teaserText: parsed.teaserText || 'A mysterious place awaits...', + locationLat: parsed.locationLat || 0, + locationLon: parsed.locationLon || 0, + isLastPointOfDay: parsed.isLastPointOfDay || false, + } + } catch { + console.error('Failed to parse generated point:', text) + return { + title: 'Mystery Spot', + teaserText: params.lang === 'ru' + ? 'Загадочное место ждёт тебя...' + : 'A mysterious place awaits...', + locationLat: 0, + locationLon: 0, + isLastPointOfDay: false, + } + } + } + + async generatePointContent(params: { + city: string + pointTitle: string + dayNumber: number + companions: string + pace: string + lang: string + }): Promise { + const systemPrompt = getSystemPrompt(params.lang) + const userPrompt = getPointContentPrompt(params) + + const response = await client.chat.completions.create({ + model: MODEL, + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: userPrompt }, + ], + temperature: 0.7, + max_tokens: 2000, + }) + + const text = response.choices[0]?.message?.content?.trim() || '' + return { contentText: text } + } + + async generateReaction(params: { + eventType: string + pointTitle?: string + city: string + lang: string + companions: string + pace: string + }): Promise { + const systemPrompt = getSystemPrompt(params.lang) + const userPrompt = getReactionPrompt(params) + + const response = await client.chat.completions.create({ + model: MODEL, + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: userPrompt }, + ], + temperature: 0.8, + max_tokens: 300, + }) + + return response.choices[0]?.message?.content?.trim() || '' + } +} + +export const aiService = new AIService() diff --git a/bot/services/event-processor.ts b/bot/services/event-processor.ts new file mode 100644 index 0000000..e0e5d03 --- /dev/null +++ b/bot/services/event-processor.ts @@ -0,0 +1,209 @@ +import type { Bot } from 'grammy' +import type { BotContext } from '../types.js' +import { + getUnprocessedEvents, markEventProcessed, + getPointById, findUserByTelegramId, + getActiveQuestForUser, getActiveDayForQuest, + updatePointContent, +} from '../../api/_lib/db.js' +import { aiService } from './ai.service.js' +import { generateNextPoint, generatePointContent } from './quest.service.js' +import { t, getLang } from '../i18n/index.js' + +let processorInterval: ReturnType | null = null + +export function startEventProcessor(bot: Bot) { + // Poll every 3 seconds for unprocessed events + processorInterval = setInterval(async () => { + try { + await processEvents(bot) + } catch (error) { + console.error('Event processor error:', error) + } + }, 3000) + + console.log('Event processor started') +} + +export function stopEventProcessor() { + if (processorInterval) { + clearInterval(processorInterval) + processorInterval = null + } +} + +async function processEvents(bot: Bot) { + const events = await getUnprocessedEvents() + + for (const event of events) { + try { + await processEvent(bot, event) + await markEventProcessed(event.id) + } catch (error) { + console.error(`Failed to process event ${event.id}:`, error) + // Still mark as processed to avoid infinite retry + await markEventProcessed(event.id) + } + } +} + +async function processEvent(bot: Bot, event: any) { + // Look up the user to get their telegram_id for sending messages + const { pool } = await import('../../api/_lib/db.js') + const { rows: users } = await pool.query('SELECT * FROM users WHERE id = $1', [event.user_id]) + const user = users[0] + if (!user) return + + const lang = getLang(user.language_code) + const quest = await getActiveQuestForUser(user.id) + if (!quest) return + + const point = event.point_id ? await getPointById(event.point_id) : null + + switch (event.event_type) { + case 'arrived': { + if (point && !point.content_text) { + // Generate full content for the point + const content = await generatePointContent(point.id, { + city: quest.city_name, + pointTitle: point.title, + dayNumber: 1, // TODO: get actual day number + companions: quest.companions, + pace: quest.pace, + lang, + }) + + if (content) { + // Send a reaction to the chat + const reaction = await aiService.generateReaction({ + eventType: 'arrived', + pointTitle: point.title, + city: quest.city_name, + lang, + companions: quest.companions, + pace: quest.pace, + }) + + if (reaction) { + await bot.api.sendMessage(user.telegram_id, reaction) + } + } + } + break + } + + case 'point_completed': { + // Generate next point + const day = await getActiveDayForQuest(quest.id) + if (!day) break + + const result = await generateNextPoint(quest.id, { + city: quest.city_name, + dayNumber: day.day_number, + totalDays: quest.number_of_days, + dayTheme: day.theme || '', + pace: quest.pace, + companions: quest.companions, + userComment: quest.user_comment, + lang, + }) + + const reaction = await aiService.generateReaction({ + eventType: 'point_completed', + pointTitle: point?.title, + city: quest.city_name, + lang, + companions: quest.companions, + pace: quest.pace, + }) + + if (reaction) { + const appUrl = process.env.APP_URL || 'https://guidly.example.com' + const isHttps = appUrl.startsWith('https://') + const msgText = isHttps ? reaction : reaction + `\n\n🔗 ${appUrl}` + await bot.api.sendMessage(user.telegram_id, msgText, isHttps ? { + reply_markup: { + inline_keyboard: [[ { text: t(lang, 'open_quest'), web_app: { url: appUrl } } ]], + }, + } : {}) + } + break + } + + case 'skipped': { + const reaction = await aiService.generateReaction({ + eventType: 'skipped', + pointTitle: point?.title, + city: quest.city_name, + lang, + companions: quest.companions, + pace: quest.pace, + }) + + if (reaction) { + await bot.api.sendMessage(user.telegram_id, reaction) + } + + // Generate next point + const day = await getActiveDayForQuest(quest.id) + if (day) { + await generateNextPoint(quest.id, { + city: quest.city_name, + dayNumber: day.day_number, + totalDays: quest.number_of_days, + dayTheme: day.theme || '', + pace: quest.pace, + companions: quest.companions, + userComment: quest.user_comment, + lang, + }) + } + break + } + + case 'finished_today': { + const reaction = await aiService.generateReaction({ + eventType: 'finished_today', + city: quest.city_name, + lang, + companions: quest.companions, + pace: quest.pace, + }) + + if (reaction) { + await bot.api.sendMessage(user.telegram_id, reaction) + } + break + } + + case 'day_completed': { + const reaction = await aiService.generateReaction({ + eventType: 'day_completed', + city: quest.city_name, + lang, + companions: quest.companions, + pace: quest.pace, + }) + + if (reaction) { + await bot.api.sendMessage(user.telegram_id, reaction) + } + break + } + + case 'quest_completed': { + const reaction = await aiService.generateReaction({ + eventType: 'quest_completed', + city: quest.city_name, + lang, + companions: quest.companions, + pace: quest.pace, + }) + + if (reaction) { + await bot.api.sendMessage(user.telegram_id, reaction) + } + break + } + } +} diff --git a/bot/services/quest.service.ts b/bot/services/quest.service.ts new file mode 100644 index 0000000..c3a872a --- /dev/null +++ b/bot/services/quest.service.ts @@ -0,0 +1,150 @@ +import { + createQuest, createQuestDay, createQuestPoint, + updateQuestStatus, updateDayStatus, + getActiveDayForQuest, getPointsForDay, +} from '../../api/_lib/db.js' +import { aiService } from './ai.service.js' + +interface OnboardingData { + userId: string + cityId: string + cityName: string + days: number + companions: string + pace: string + userComment?: string + lang: string +} + +export async function createQuestFromOnboarding(data: OnboardingData) { + try { + // Generate quest plan via AI + const plan = await aiService.generateQuestPlan({ + city: data.cityName, + days: data.days, + companions: data.companions, + pace: data.pace, + userComment: data.userComment, + lang: data.lang, + }) + + // Create quest in DB + const quest = await createQuest({ + city_id: data.cityId, + user_id: data.userId, + title: plan.title, + description: plan.description, + number_of_days: data.days, + pace: data.pace, + companions: data.companions, + user_comment: data.userComment, + }) + + // Create day skeletons + const days = [] + for (let i = 0; i < data.days; i++) { + const day = await createQuestDay({ + quest_id: quest.id, + day_number: i + 1, + theme: plan.dayThemes[i] || undefined, + }) + days.push(day) + } + + // Start the quest and first day + await updateQuestStatus(quest.id, 'in_progress') + await updateDayStatus(days[0].id, 'in_progress') + + // Generate first point + const firstPoint = await aiService.generatePoint({ + city: data.cityName, + dayNumber: 1, + totalDays: data.days, + dayTheme: plan.dayThemes[0] || '', + pace: data.pace, + companions: data.companions, + userComment: data.userComment, + previousPoints: [], + lang: data.lang, + }) + + await createQuestPoint({ + day_id: days[0].id, + title: firstPoint.title, + location_lat: firstPoint.locationLat, + location_lon: firstPoint.locationLon, + teaser_text: firstPoint.teaserText, + order_in_day: 1, + status: 'active', + }) + + return { + questId: quest.id, + title: plan.title, + description: plan.description, + } + } catch (error) { + console.error('Failed to create quest from onboarding:', error) + return null + } +} + +export async function generateNextPoint(questId: string, questContext: { + city: string + dayNumber: number + totalDays: number + dayTheme: string + pace: string + companions: string + userComment?: string + lang: string +}) { + try { + const day = await getActiveDayForQuest(questId) + if (!day) return null + + const existingPoints = await getPointsForDay(day.id) + const previousPoints = existingPoints.map((p: any) => p.title) + const maxOrder = existingPoints.reduce((max: number, p: any) => Math.max(max, p.order_in_day || 0), 0) + const nextOrder = maxOrder + 1 + + const point = await aiService.generatePoint({ + ...questContext, + previousPoints, + }) + + const created = await createQuestPoint({ + day_id: day.id, + title: point.title, + location_lat: point.locationLat, + location_lon: point.locationLon, + teaser_text: point.teaserText, + order_in_day: nextOrder, + status: 'active', + }) + + return { point: created, isLastOfDay: point.isLastPointOfDay } + } catch (error) { + console.error('Failed to generate next point:', error) + return null + } +} + +export async function generatePointContent(pointId: string, context: { + city: string + pointTitle: string + dayNumber: number + companions: string + pace: string + lang: string +}) { + try { + const content = await aiService.generatePointContent(context) + const { updatePointContent } = await import('../../api/_lib/db.js') + await updatePointContent(pointId, content.contentText) + return content + } catch (error) { + console.error('Failed to generate point content:', error) + return null + } +} diff --git a/bot/types.ts b/bot/types.ts new file mode 100644 index 0000000..bf85cb5 --- /dev/null +++ b/bot/types.ts @@ -0,0 +1,40 @@ +import type { Context, SessionFlavor } from 'grammy' +import type { ConversationFlavor } from '@grammyjs/conversations' + +export interface SessionData { + conversationState: 'idle' | 'onboarding' | 'quest_active' | 'waiting_confirmation' + userId?: string // DB user ID + + onboarding?: { + cityId?: string + cityName?: string + days?: number + companions?: 'solo' | 'couple' | 'family' | 'friends' + pace?: 'slow' | 'normal' | 'active' + comment?: string + step: number + } + + activeQuestId?: string + activePointId?: string +} + +export type BotContext = ConversationFlavor> + +export interface QuestPlan { + title: string + description: string + dayThemes: string[] +} + +export interface GeneratedPoint { + title: string + teaserText: string + locationLat: number + locationLon: number + isLastPointOfDay: boolean +} + +export interface PointContent { + contentText: string +} diff --git a/db/migrations/001_initial.sql b/db/migrations/001_initial.sql new file mode 100644 index 0000000..5c465e7 --- /dev/null +++ b/db/migrations/001_initial.sql @@ -0,0 +1,170 @@ +-- Guidly: Initial database schema +-- Run with: npm run migrate + +-- Enable UUID generation +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; + +-- ============================================ +-- Cities +-- ============================================ +CREATE TABLE cities ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + country VARCHAR(100) NOT NULL, + description TEXT, + status VARCHAR(20) NOT NULL DEFAULT 'active' + CHECK (status IN ('active', 'inactive')), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- ============================================ +-- Users +-- ============================================ +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + telegram_id BIGINT NOT NULL UNIQUE, + username VARCHAR(255), + display_name VARCHAR(255), + language_code VARCHAR(10) DEFAULT 'ru', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_users_telegram_id ON users(telegram_id); + +-- ============================================ +-- Quests +-- ============================================ +CREATE TABLE quests ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + city_id UUID NOT NULL REFERENCES cities(id), + user_id UUID NOT NULL REFERENCES users(id), + title TEXT, + description TEXT, + number_of_days INT NOT NULL, + pace VARCHAR(20) NOT NULL CHECK (pace IN ('slow', 'normal', 'active')), + companions VARCHAR(20) NOT NULL CHECK (companions IN ('solo', 'couple', 'family', 'friends')), + user_comment TEXT, + status VARCHAR(30) NOT NULL DEFAULT 'not_started' + CHECK (status IN ('not_started', 'in_progress', 'completed')), + started_at TIMESTAMPTZ, + completed_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_quests_user_id ON quests(user_id); +CREATE INDEX idx_quests_user_status ON quests(user_id, status); + +-- ============================================ +-- Quest Days +-- ============================================ +CREATE TABLE quest_days ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + quest_id UUID NOT NULL REFERENCES quests(id) ON DELETE CASCADE, + day_number INT NOT NULL, + theme TEXT, + status VARCHAR(20) NOT NULL DEFAULT 'not_started' + CHECK (status IN ('not_started', 'in_progress', 'completed')), + started_at TIMESTAMPTZ, + completed_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + UNIQUE (quest_id, day_number) +); + +CREATE INDEX idx_quest_days_quest ON quest_days(quest_id); + +-- ============================================ +-- Quest Points (individual stops/locations) +-- ============================================ +CREATE TABLE quest_points ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + day_id UUID NOT NULL REFERENCES quest_days(id) ON DELETE CASCADE, + title VARCHAR(500) NOT NULL, + location_lat DECIMAL(10, 7), + location_lon DECIMAL(10, 7), + teaser_text TEXT, + content_text TEXT, + order_in_day INT NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'hidden' + CHECK (status IN ('hidden', 'preview', 'active', 'completed', 'skipped')), + arrived_at TIMESTAMPTZ, + completed_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + UNIQUE (day_id, order_in_day) +); + +CREATE INDEX idx_quest_points_day ON quest_points(day_id); + +-- ============================================ +-- Quest Events (Mini App -> Bot communication) +-- ============================================ +CREATE TABLE quest_events ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + quest_id UUID NOT NULL REFERENCES quests(id), + user_id UUID NOT NULL REFERENCES users(id), + point_id UUID REFERENCES quest_points(id), + event_type VARCHAR(30) NOT NULL + CHECK (event_type IN ( + 'arrived', 'photo_uploaded', 'skipped', + 'finished_today', 'point_completed', + 'quest_started', 'quest_completed', + 'day_started', 'day_completed', + 'peek_next' + )), + payload JSONB DEFAULT '{}', + processed BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_quest_events_quest ON quest_events(quest_id); +CREATE INDEX idx_quest_events_unprocessed ON quest_events(processed) WHERE processed = false; + +-- ============================================ +-- Chat History +-- ============================================ +CREATE TABLE chat_history ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id), + quest_id UUID NOT NULL REFERENCES quests(id), + point_id UUID REFERENCES quest_points(id), + message_type VARCHAR(20) NOT NULL + CHECK (message_type IN ('user_text', 'user_voice', 'ai_text', 'ai_audio')), + content TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_chat_history_quest ON chat_history(quest_id); + +-- ============================================ +-- Achievements +-- ============================================ +CREATE TABLE achievements ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id), + type VARCHAR(50) NOT NULL + CHECK (type IN ( + 'first_quest', 'quest_completed', + 'cities_2', 'cities_5', 'cities_10', + 'steps_50', 'steps_100' + )), + city_id UUID REFERENCES cities(id), + achieved_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + UNIQUE (user_id, type, city_id) +); + +CREATE INDEX idx_achievements_user ON achievements(user_id); + +-- ============================================ +-- Seed: Test cities +-- ============================================ +INSERT INTO cities (name, country, description) VALUES + ('Novosibirsk', 'Russia', 'The cultural and scientific capital of Siberia'), + ('Moscow', 'Russia', 'The heart of Russia with centuries of history'), + ('Saint Petersburg', 'Russia', 'The cultural capital with imperial grandeur'), + ('Istanbul', 'Turkey', 'Where East meets West across the Bosphorus'), + ('Barcelona', 'Spain', 'A city of art, architecture and Mediterranean spirit'); diff --git a/deploy/deploy.sh b/deploy/deploy.sh new file mode 100644 index 0000000..1bddff3 --- /dev/null +++ b/deploy/deploy.sh @@ -0,0 +1,57 @@ +#!/bin/bash +set -e + +SERVER="${DEPLOY_SERVER:-root@85.239.58.182}" +REMOTE_DIR="/root/guidly" +DOMAIN="gidli.ru" + +echo "=== Building project ===" +npm run build + +echo "=== Creating deployment archive ===" +tar -czf deploy.tar.gz \ + dist/ \ + dist-server/ \ + dist-bot/ \ + package.json \ + package-lock.json \ + ecosystem.config.cjs \ + deploy/nginx-guidly.conf + +echo "=== Uploading to server ===" +scp deploy.tar.gz "$SERVER:/tmp/" + +echo "=== Deploying on server ===" +ssh "$SERVER" << ENDSSH +set -e +mkdir -p $REMOTE_DIR +cd $REMOTE_DIR + +# Extract new files +tar -xzf /tmp/deploy.tar.gz +rm /tmp/deploy.tar.gz + +# Install production dependencies +npm install --omit=dev + +# Copy nginx config if not already linked +if [ ! -f /etc/nginx/sites-available/$DOMAIN ]; then + cp deploy/nginx-guidly.conf /etc/nginx/sites-available/$DOMAIN + ln -sf /etc/nginx/sites-available/$DOMAIN /etc/nginx/sites-enabled/ + rm -f /etc/nginx/sites-enabled/default + nginx -t && systemctl reload nginx + echo "Nginx config installed. Run 'certbot --nginx -d $DOMAIN' if SSL not yet set up." +fi + +# Restart app via PM2 +pm2 delete guidly-server 2>/dev/null || true +pm2 start ecosystem.config.cjs +pm2 save + +echo "=== Deployment complete ===" +echo "App: https://$DOMAIN" +ENDSSH + +echo "=== Cleanup ===" +rm deploy.tar.gz +echo "=== Deploy complete ===" diff --git a/deploy/nginx-guidly.conf b/deploy/nginx-guidly.conf new file mode 100644 index 0000000..7b2a620 --- /dev/null +++ b/deploy/nginx-guidly.conf @@ -0,0 +1,43 @@ +server { + listen 80; + server_name gidli.ru; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + server_name gidli.ru; + + # SSL certificates (managed by certbot) + ssl_certificate /etc/letsencrypt/live/gidli.ru/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/gidli.ru/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + # Security headers + add_header X-Frame-Allow "tg-embed"; + add_header X-Content-Type-Options nosniff; + + # Proxy API and webhook requests to Express + location /api/ { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 120s; + } + + # Serve static files (Vite build output) + location / { + root /root/guidly/dist; + try_files $uri $uri/ /index.html; + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { + expires 30d; + add_header Cache-Control "public, immutable"; + } + } +} diff --git a/deploy/setup-server.sh b/deploy/setup-server.sh new file mode 100644 index 0000000..61bb02c --- /dev/null +++ b/deploy/setup-server.sh @@ -0,0 +1,49 @@ +#!/bin/bash +set -e + +echo "=== Guidly VPS Setup ===" +echo "Run this script once on a fresh Ubuntu 22/24 server" + +# 1. Update system +echo ">>> Updating system..." +apt update && apt upgrade -y + +# 2. Install Node.js 22 (LTS) +echo ">>> Installing Node.js 22..." +curl -fsSL https://deb.nodesource.com/setup_22.x | bash - +apt install -y nodejs + +# 3. Install nginx +echo ">>> Installing nginx..." +apt install -y nginx + +# 4. Install certbot for Let's Encrypt SSL +echo ">>> Installing certbot..." +apt install -y certbot python3-certbot-nginx + +# 5. Install PM2 globally +echo ">>> Installing PM2..." +npm install -g pm2 + +# 6. Create app directory +echo ">>> Creating app directory..." +mkdir -p /root/guidly + +# 7. Configure firewall (if ufw is active) +if command -v ufw &> /dev/null; then + echo ">>> Configuring firewall..." + ufw allow 'Nginx Full' + ufw allow OpenSSH +fi + +echo "" +echo "=== Setup complete ===" +echo "" +echo "Next steps:" +echo " 1. Point DNS: gidli.ru → $(curl -s ifconfig.me)" +echo " 2. Copy nginx config: cp /root/guidly/deploy/nginx-guidly.conf /etc/nginx/sites-available/gidli.ru" +echo " 3. Enable site: ln -s /etc/nginx/sites-available/gidli.ru /etc/nginx/sites-enabled/" +echo " 4. Remove default: rm -f /etc/nginx/sites-enabled/default" +echo " 5. Get SSL cert: certbot --nginx -d gidli.ru" +echo " 6. Test nginx: nginx -t && systemctl reload nginx" +echo " 7. Deploy app: run deploy.sh from your local machine" diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs new file mode 100644 index 0000000..ce3c4ce --- /dev/null +++ b/ecosystem.config.cjs @@ -0,0 +1,30 @@ +module.exports = { + apps: [ + { + name: 'guidly-server', + script: 'dist-server/server/index.js', + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: '500M', + env: { + NODE_ENV: 'production', + PORT: 3000, + }, + }, + // Bot is integrated into the server process in webhook mode (production). + // This separate bot process is only needed for polling mode (development/fallback). + // Uncomment if you need to run bot separately: + // { + // name: 'guidly-bot', + // script: 'dist-bot/bot/index.js', + // instances: 1, + // autorestart: true, + // watch: false, + // max_memory_restart: '300M', + // env: { + // NODE_ENV: 'production', + // }, + // }, + ], +} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..03b1d8a --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist', 'dist-server', 'dist-bot', 'node_modules', 'api'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/index.html b/index.html new file mode 100644 index 0000000..2a406ff --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + + + Guidly + + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4fb38a1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5459 @@ +{ + "name": "guidly", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "guidly", + "version": "0.1.0", + "dependencies": { + "@grammyjs/conversations": "^2.0.3", + "cookie-parser": "^1.4.7", + "dotenv": "^17.2.4", + "express": "^5.2.1", + "grammy": "^1.35.0", + "openai": "^4.80.0", + "pg": "^8.17.2", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.0.0", + "zod": "^3.24.0", + "zustand": "^5.0.0" + }, + "devDependencies": { + "@eslint/js": "^9.17.0", + "@types/cookie-parser": "^1.4.10", + "@types/express": "^5.0.6", + "@types/pg": "^8.16.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.4", + "concurrently": "^9.2.1", + "eslint": "^9.17.0", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.16", + "globals": "^15.13.0", + "tsx": "^4.21.0", + "typescript": "~5.6.2", + "typescript-eslint": "^8.18.2", + "vite": "^6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@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-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-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "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.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "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/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.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/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "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/eslint-utils/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-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@grammyjs/conversations": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@grammyjs/conversations/-/conversations-2.1.1.tgz", + "integrity": "sha512-hoxqwSkaXDeU7mzXulpk3A4Cmd6UZO3HU4aPoITX5ekSHK7ZcUEmMl7RhKKkqw3z6zVbbAShQreJoVV5/dDSLA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14.13.1" + }, + "peerDependencies": { + "grammy": "^1.20.1" + } + }, + "node_modules/@grammyjs/types": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@grammyjs/types/-/types-3.24.0.tgz", + "integrity": "sha512-qQIEs4lN5WqUdr4aT8MeU6UFpMbGYAvcvYSW1A4OO1PABGJQHz/KLON6qvpf+5RxaNDQBxiY2k2otIhg/AG7RQ==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "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/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "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/@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.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", + "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, + "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/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/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/node": { + "version": "25.2.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.2.tgz", + "integrity": "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/@types/pg": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.16.0.tgz", + "integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.13", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz", + "integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.55.0.tgz", + "integrity": "sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.55.0", + "@typescript-eslint/type-utils": "8.55.0", + "@typescript-eslint/utils": "8.55.0", + "@typescript-eslint/visitor-keys": "8.55.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.55.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.55.0.tgz", + "integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.55.0", + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/typescript-estree": "8.55.0", + "@typescript-eslint/visitor-keys": "8.55.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.55.0.tgz", + "integrity": "sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.55.0", + "@typescript-eslint/types": "^8.55.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.55.0.tgz", + "integrity": "sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/visitor-keys": "8.55.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.55.0.tgz", + "integrity": "sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.55.0.tgz", + "integrity": "sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/typescript-estree": "8.55.0", + "@typescript-eslint/utils": "8.55.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.55.0.tgz", + "integrity": "sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.55.0.tgz", + "integrity": "sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.55.0", + "@typescript-eslint/tsconfig-utils": "8.55.0", + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/visitor-keys": "8.55.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/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/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "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/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.55.0.tgz", + "integrity": "sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.55.0", + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/typescript-estree": "8.55.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.55.0.tgz", + "integrity": "sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.55.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "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/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==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 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/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/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/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "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.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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/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.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "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": "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/chalk/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/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "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/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==", + "license": "MIT", + "engines": { + "node": ">=0.4.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/dotenv": { + "version": "17.2.4", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.4.tgz", + "integrity": "sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==", + "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/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "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/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "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-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/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": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "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 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "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/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "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-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/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "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/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/form-data/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/form-data/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/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "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": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "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/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "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/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/grammy": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/grammy/-/grammy-1.40.0.tgz", + "integrity": "sha512-ssuE7fc1AwqlUxHr931OCVW3fU+oFDjHZGgvIedPKXfTdjXvzP19xifvVGCnPtYVUig1Kz+gwxe4A9M5WdkT4Q==", + "license": "MIT", + "dependencies": { + "@grammyjs/types": "3.24.0", + "abort-controller": "^3.0.0", + "debug": "^4.4.3", + "node-fetch": "^2.7.0" + }, + "engines": { + "node": "^12.20.0 || >=14.13.1" + } + }, + "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==", + "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/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/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/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-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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/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/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "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/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/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/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.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/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/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": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "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/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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", + "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": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "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.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "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/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openai": { + "version": "4.104.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", + "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/openai/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/openai/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "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/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-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-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pg": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", + "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.11.0", + "pg-pool": "^3.11.0", + "pg-protocol": "^1.11.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.11.0.tgz", + "integrity": "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz", + "integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz", + "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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/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/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/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "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": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "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/react-router": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz", + "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz", + "integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "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.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "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/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/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/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/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "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/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": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "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==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "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-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.55.0.tgz", + "integrity": "sha512-HE4wj+r5lmDVS9gdaN0/+iqNvPZwGfnJ5lZuz7s5vLlg9ODw0bIiiETaios9LvFI1U94/VBXGm3CB2Y5cNFMpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.55.0", + "@typescript-eslint/parser": "8.55.0", + "@typescript-eslint/typescript-estree": "8.55.0", + "@typescript-eslint/utils": "8.55.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/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/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.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 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "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==", + "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==", + "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/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f7bbd18 --- /dev/null +++ b/package.json @@ -0,0 +1,48 @@ +{ + "name": "guidly", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "concurrently \"vite\" \"tsx watch bot/index.ts\" \"tsx watch server/index.ts\"", + "dev:bot": "tsx watch bot/index.ts", + "dev:app": "vite", + "build": "tsc -b && tsc -p tsconfig.server.json && tsc -p tsconfig.bot.json && vite build", + "build:bot": "tsc -p tsconfig.bot.json", + "lint": "eslint .", + "preview": "vite preview", + "migrate": "tsx scripts/migrate.ts" + }, + "dependencies": { + "@grammyjs/conversations": "^2.0.3", + "cookie-parser": "^1.4.7", + "dotenv": "^17.2.4", + "express": "^5.2.1", + "grammy": "^1.35.0", + "openai": "^4.80.0", + "pg": "^8.17.2", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.0.0", + "zod": "^3.24.0", + "zustand": "^5.0.0" + }, + "devDependencies": { + "@eslint/js": "^9.17.0", + "@types/cookie-parser": "^1.4.10", + "@types/express": "^5.0.6", + "@types/pg": "^8.16.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.4", + "concurrently": "^9.2.1", + "eslint": "^9.17.0", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.16", + "globals": "^15.13.0", + "tsx": "^4.21.0", + "typescript": "~5.6.2", + "typescript-eslint": "^8.18.2", + "vite": "^6.0.0" + } +} diff --git a/scripts/migrate.ts b/scripts/migrate.ts new file mode 100644 index 0000000..af089d5 --- /dev/null +++ b/scripts/migrate.ts @@ -0,0 +1,66 @@ +import 'dotenv/config' +import { readFileSync, readdirSync } from 'fs' +import { join, dirname } from 'path' +import { fileURLToPath } from 'url' +import pg from 'pg' + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +const pool = new pg.Pool({ + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT || '5432'), + database: process.env.DB_NAME || 'guidly', + user: process.env.DB_USER || 'guidly', + password: process.env.DB_PASSWORD || '', + ssl: process.env.DATABASE_SSL === 'false' ? false : undefined, +}) + +async function migrate() { + console.log('Running migrations...') + + // Create migrations tracking table + await pool.query(` + CREATE TABLE IF NOT EXISTS _migrations ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL UNIQUE, + executed_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ) + `) + + // Get already executed migrations + const { rows: executed } = await pool.query('SELECT name FROM _migrations ORDER BY name') + const executedNames = new Set(executed.map((r: { name: string }) => r.name)) + + // Read migration files + const migrationsDir = join(__dirname, '..', 'db', 'migrations') + const files = readdirSync(migrationsDir) + .filter(f => f.endsWith('.sql')) + .sort() + + for (const file of files) { + if (executedNames.has(file)) { + console.log(` [skip] ${file} (already executed)`) + continue + } + + const sql = readFileSync(join(migrationsDir, file), 'utf-8') + console.log(` [run] ${file}`) + + try { + await pool.query(sql) + await pool.query('INSERT INTO _migrations (name) VALUES ($1)', [file]) + console.log(` [ok] ${file}`) + } catch (error: any) { + console.error(` [fail] ${file}:`, error.message) + process.exit(1) + } + } + + console.log('Migrations complete!') + await pool.end() +} + +migrate().catch((error) => { + console.error('Migration failed:', error) + process.exit(1) +}) diff --git a/server/index.ts b/server/index.ts new file mode 100644 index 0000000..1c131a0 --- /dev/null +++ b/server/index.ts @@ -0,0 +1,117 @@ +import 'dotenv/config' +import express from 'express' +import cookieParser from 'cookie-parser' +import path from 'path' +import { fileURLToPath } from 'url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const app = express() +const PORT = process.env.PORT || 3000 + +// Middleware +app.use(express.json({ limit: '10mb' })) +app.use(express.urlencoded({ extended: true })) +app.use(cookieParser()) + +// Wrap handler to catch errors and merge params +function wrapHandler(handler: Function) { + return async (req: any, res: any, next: any) => { + const mergedQuery = { ...req.query, ...req.params } + Object.defineProperty(req, 'query', { + value: mergedQuery, + writable: true, + configurable: true, + }) + try { + await handler(req, res) + } catch (error) { + next(error) + } + } +} + +async function setupRoutes() { + // Quest API (Mini App) + const questCurrent = (await import('../api/quest/current.js')).default + const questEvents = (await import('../api/quest/events.js')).default + const questHistory = (await import('../api/quest/history.js')).default + + // Internal API (Bot) + const botInternal = (await import('../api/bot/internal.js')).default + + // Quest routes (Mini App) + app.all('/api/quest/current', wrapHandler(questCurrent)) + app.all('/api/quest/events', wrapHandler(questEvents)) + app.all('/api/quest/history', wrapHandler(questHistory)) + app.all('/api/quest/history/:id', wrapHandler(questHistory)) + + // Internal bot routes + app.all('/api/bot/internal', wrapHandler(botInternal)) + + // In production: start bot in webhook mode inside this server process + const isProduction = process.env.NODE_ENV === 'production' && process.env.BOT_WEBHOOK_URL + if (isProduction) { + const { webhookCallback } = await import('grammy') + const { createBot } = await import('../bot/bot.js') + const { startEventProcessor } = await import('../bot/services/event-processor.js') + + const token = process.env.TELEGRAM_BOT_TOKEN + if (!token) { + console.error('TELEGRAM_BOT_TOKEN is not set!') + process.exit(1) + } + + const bot = createBot(token) + await bot.init() + + // Register grammY webhook handler + app.post('/api/telegram/webhook', webhookCallback(bot, 'express')) + + // Set webhook with Telegram + await bot.api.setWebhook(process.env.BOT_WEBHOOK_URL!, { + secret_token: process.env.BOT_WEBHOOK_SECRET, + }) + + // Start event processing loop + startEventProcessor(bot) + + console.log('Bot started in webhook mode') + } else { + // In development: register a placeholder webhook route + const telegramWebhook = (await import('../api/telegram/webhook.js')).default + app.all('/api/telegram/webhook', wrapHandler(telegramWebhook)) + } + + // Serve static files from dist/ in production + const distPath = path.join(__dirname, '..', '..', 'dist') + app.use(express.static(distPath)) + + // SPA fallback + app.use((req: any, res: any, next: any) => { + if (req.path.startsWith('/api/')) { + return next() + } + res.sendFile(path.join(distPath, 'index.html')) + }) +} + +// Error handler +app.use((err: any, _req: any, res: any, _next: any) => { + console.error('Server error:', err) + res.status(500).json({ error: 'Internal server error' }) +}) + +async function start() { + try { + await setupRoutes() + app.listen(PORT, () => { + console.log(`Guidly server running on http://localhost:${PORT}`) + }) + } catch (error) { + console.error('Failed to start server:', error) + process.exit(1) + } +} + +start() diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..cf6f8ab --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,17 @@ +import { BrowserRouter, Routes, Route } from 'react-router-dom' +import { QuestPage } from './pages/QuestPage' +import { HistoryPage } from './pages/HistoryPage' + +function App() { + return ( + + + } /> + } /> + } /> + + + ) +} + +export default App diff --git a/src/components/quest/ActionButtons.module.css b/src/components/quest/ActionButtons.module.css new file mode 100644 index 0000000..9f1ee8a --- /dev/null +++ b/src/components/quest/ActionButtons.module.css @@ -0,0 +1,16 @@ +.container { + padding: 16px; + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: var(--color-bg); + border-top: 1px solid var(--color-surface); +} + +.secondary { + display: flex; + justify-content: center; + gap: 12px; + margin-top: 10px; +} diff --git a/src/components/quest/ActionButtons.tsx b/src/components/quest/ActionButtons.tsx new file mode 100644 index 0000000..a1e0213 --- /dev/null +++ b/src/components/quest/ActionButtons.tsx @@ -0,0 +1,95 @@ +import { useState } from 'react' +import styles from './ActionButtons.module.css' +import { Button } from '@/components/ui/Button' +import { getLanguage } from '@/utils/telegram' + +interface ActionButtonsProps { + hasArrived: boolean + hasContent: boolean + onArrived: () => void + onCompleted: () => void + onSkipped: () => void + onFinishedToday: () => void +} + +export function ActionButtons({ + hasArrived, + hasContent, + onArrived, + onCompleted, + onSkipped, + onFinishedToday, +}: ActionButtonsProps) { + const lang = getLanguage() + const [loading, setLoading] = useState(null) + + const handleAction = async (action: string, handler: () => void) => { + setLoading(action) + try { + await handler() + } finally { + setLoading(null) + } + } + + if (hasArrived && hasContent) { + // After arrival and content is shown — "Next" button + return ( +
+ +
+ ) + } + + if (hasArrived && !hasContent) { + // Arrived but content loading + return ( +
+ +
+ ) + } + + // Before arrival + return ( +
+ + +
+ + + +
+
+ ) +} diff --git a/src/components/quest/ContentView.module.css b/src/components/quest/ContentView.module.css new file mode 100644 index 0000000..053e566 --- /dev/null +++ b/src/components/quest/ContentView.module.css @@ -0,0 +1,17 @@ +.container { + padding: 20px; + margin: 0 16px; + background: var(--color-surface); + border-radius: var(--radius); +} + +.paragraph { + font-size: 15px; + line-height: 1.7; + margin-bottom: 16px; + color: var(--color-text); +} + +.paragraph:last-child { + margin-bottom: 0; +} diff --git a/src/components/quest/ContentView.tsx b/src/components/quest/ContentView.tsx new file mode 100644 index 0000000..16a1ca9 --- /dev/null +++ b/src/components/quest/ContentView.tsx @@ -0,0 +1,20 @@ +import styles from './ContentView.module.css' + +interface ContentViewProps { + content: string +} + +export function ContentView({ content }: ContentViewProps) { + // Split content into paragraphs + const paragraphs = content.split('\n').filter(p => p.trim()) + + return ( +
+ {paragraphs.map((paragraph, index) => ( +

+ {paragraph} +

+ ))} +
+ ) +} diff --git a/src/components/quest/MapLink.module.css b/src/components/quest/MapLink.module.css new file mode 100644 index 0000000..e53bd15 --- /dev/null +++ b/src/components/quest/MapLink.module.css @@ -0,0 +1,26 @@ +.container { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 0; +} + +.link { + color: var(--color-link); + font-size: 14px; + font-weight: 500; + text-decoration: none; + padding: 6px 12px; + background: var(--color-surface); + border-radius: var(--radius-sm); + transition: opacity 0.2s; +} + +.link:active { + opacity: 0.7; +} + +.separator { + color: var(--color-hint); + font-size: 12px; +} diff --git a/src/components/quest/MapLink.tsx b/src/components/quest/MapLink.tsx new file mode 100644 index 0000000..c9bedbf --- /dev/null +++ b/src/components/quest/MapLink.tsx @@ -0,0 +1,37 @@ +import styles from './MapLink.module.css' +import { getLanguage } from '@/utils/telegram' + +interface MapLinkProps { + lat: number + lon: number + title: string +} + +export function MapLink({ lat, lon }: MapLinkProps) { + const lang = getLanguage() + + const googleMapsUrl = `https://www.google.com/maps/search/?api=1&query=${lat},${lon}` + const yandexMapsUrl = `https://yandex.ru/maps/?pt=${lon},${lat}&z=17&l=map` + + return ( + + ) +} diff --git a/src/components/quest/StepCard.module.css b/src/components/quest/StepCard.module.css new file mode 100644 index 0000000..e468992 --- /dev/null +++ b/src/components/quest/StepCard.module.css @@ -0,0 +1,19 @@ +.card { + padding: 20px; + background: var(--color-surface); + border-radius: var(--radius); + margin: 0 16px; +} + +.title { + font-size: 22px; + font-weight: 700; + line-height: 1.3; + margin-bottom: 12px; +} + +.teaser { + font-size: 15px; + line-height: 1.6; + color: var(--color-hint); +} diff --git a/src/components/quest/StepCard.tsx b/src/components/quest/StepCard.tsx new file mode 100644 index 0000000..f01c411 --- /dev/null +++ b/src/components/quest/StepCard.tsx @@ -0,0 +1,30 @@ +import styles from './StepCard.module.css' +import type { QuestPoint } from '@/types' +import { MapLink } from './MapLink' + +interface StepCardProps { + point: QuestPoint + showTeaser?: boolean +} + +export function StepCard({ point, showTeaser = true }: StepCardProps) { + const hasCoords = point.location_lat && point.location_lon + + return ( +
+

{point.title}

+ + {showTeaser && point.teaser_text && ( +

{point.teaser_text}

+ )} + + {hasCoords && ( + + )} +
+ ) +} diff --git a/src/components/quest/StepProgress.module.css b/src/components/quest/StepProgress.module.css new file mode 100644 index 0000000..f29cbb6 --- /dev/null +++ b/src/components/quest/StepProgress.module.css @@ -0,0 +1,34 @@ +.container { + padding: 16px 20px; +} + +.info { + display: flex; + justify-content: space-between; + margin-bottom: 8px; +} + +.day { + font-weight: 600; + font-size: 14px; + color: var(--color-primary); +} + +.step { + font-size: 13px; + color: var(--color-hint); +} + +.bar { + height: 4px; + background: var(--color-surface); + border-radius: 2px; + overflow: hidden; +} + +.fill { + height: 100%; + background: var(--color-primary); + border-radius: 2px; + transition: width 0.5s ease; +} diff --git a/src/components/quest/StepProgress.tsx b/src/components/quest/StepProgress.tsx new file mode 100644 index 0000000..8b2fef5 --- /dev/null +++ b/src/components/quest/StepProgress.tsx @@ -0,0 +1,32 @@ +import styles from './StepProgress.module.css' +import { getLanguage } from '@/utils/telegram' + +interface StepProgressProps { + current: number + total: number + dayNumber: number +} + +export function StepProgress({ current, total, dayNumber }: StepProgressProps) { + const lang = getLanguage() + const progress = total > 0 ? (current / total) * 100 : 0 + + return ( +
+
+ + {lang === 'ru' ? `День ${dayNumber}` : `Day ${dayNumber}`} + + + {lang === 'ru' + ? `Точка ${current} из ${total}` + : `Point ${current} of ${total}` + } + +
+
+
+
+
+ ) +} diff --git a/src/components/quest/index.ts b/src/components/quest/index.ts new file mode 100644 index 0000000..0ed5688 --- /dev/null +++ b/src/components/quest/index.ts @@ -0,0 +1,5 @@ +export { StepCard } from './StepCard' +export { StepProgress } from './StepProgress' +export { ActionButtons } from './ActionButtons' +export { MapLink } from './MapLink' +export { ContentView } from './ContentView' diff --git a/src/components/ui/Button.module.css b/src/components/ui/Button.module.css new file mode 100644 index 0000000..d665350 --- /dev/null +++ b/src/components/ui/Button.module.css @@ -0,0 +1,75 @@ +.button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + border-radius: var(--radius-sm); + font-weight: 600; + transition: opacity 0.2s, transform 0.1s; + white-space: nowrap; +} + +.button:active { + transform: scale(0.97); +} + +.button:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +/* Variants */ +.primary { + background: var(--color-primary); + color: var(--color-primary-text); +} + +.secondary { + background: var(--color-surface); + color: var(--color-text); +} + +.danger { + background: #ef4444; + color: #fff; +} + +.ghost { + background: transparent; + color: var(--color-hint); +} + +/* Sizes */ +.sm { + padding: 6px 12px; + font-size: 13px; +} + +.md { + padding: 10px 20px; + font-size: 15px; +} + +.lg { + padding: 14px 28px; + font-size: 16px; +} + +.fullWidth { + width: 100%; +} + +/* Spinner */ +.spinner { + width: 18px; + height: 18px; + border: 2px solid transparent; + border-top-color: currentColor; + border-radius: 50%; + animation: spin 0.6s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx new file mode 100644 index 0000000..bfb10f5 --- /dev/null +++ b/src/components/ui/Button.tsx @@ -0,0 +1,31 @@ +import styles from './Button.module.css' + +interface ButtonProps { + children: React.ReactNode + onClick?: () => void + variant?: 'primary' | 'secondary' | 'danger' | 'ghost' + size?: 'sm' | 'md' | 'lg' + disabled?: boolean + loading?: boolean + fullWidth?: boolean +} + +export function Button({ + children, + onClick, + variant = 'primary', + size = 'md', + disabled = false, + loading = false, + fullWidth = false, +}: ButtonProps) { + return ( + + ) +} diff --git a/src/components/ui/ErrorState.module.css b/src/components/ui/ErrorState.module.css new file mode 100644 index 0000000..b7d159f --- /dev/null +++ b/src/components/ui/ErrorState.module.css @@ -0,0 +1,15 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 20px; + gap: 16px; + text-align: center; +} + +.message { + color: var(--color-hint); + font-size: 15px; + line-height: 1.5; +} diff --git a/src/components/ui/ErrorState.tsx b/src/components/ui/ErrorState.tsx new file mode 100644 index 0000000..bf56bce --- /dev/null +++ b/src/components/ui/ErrorState.tsx @@ -0,0 +1,15 @@ +import styles from './ErrorState.module.css' +import { Button } from './Button' + +export function ErrorState({ message, onRetry }: { message: string; onRetry?: () => void }) { + return ( +
+

{message}

+ {onRetry && ( + + )} +
+ ) +} diff --git a/src/components/ui/Loader.module.css b/src/components/ui/Loader.module.css new file mode 100644 index 0000000..5df3f8c --- /dev/null +++ b/src/components/ui/Loader.module.css @@ -0,0 +1,26 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 20px; + gap: 16px; +} + +.spinner { + width: 32px; + height: 32px; + border: 3px solid var(--color-surface); + border-top-color: var(--color-primary); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +.text { + color: var(--color-hint); + font-size: 14px; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} diff --git a/src/components/ui/Loader.tsx b/src/components/ui/Loader.tsx new file mode 100644 index 0000000..30f1e93 --- /dev/null +++ b/src/components/ui/Loader.tsx @@ -0,0 +1,10 @@ +import styles from './Loader.module.css' + +export function Loader({ text }: { text?: string }) { + return ( +
+
+ {text &&

{text}

} +
+ ) +} diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts new file mode 100644 index 0000000..e0c26eb --- /dev/null +++ b/src/components/ui/index.ts @@ -0,0 +1,3 @@ +export { Button } from './Button' +export { Loader } from './Loader' +export { ErrorState } from './ErrorState' diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..939fceb --- /dev/null +++ b/src/index.css @@ -0,0 +1,54 @@ +:root { + --tg-theme-bg-color: var(--tg-theme-bg-color, #1a1a2e); + --tg-theme-text-color: var(--tg-theme-text-color, #e0e0e0); + --tg-theme-hint-color: var(--tg-theme-hint-color, #999); + --tg-theme-link-color: var(--tg-theme-link-color, #64b5f6); + --tg-theme-button-color: var(--tg-theme-button-color, #5865f2); + --tg-theme-button-text-color: var(--tg-theme-button-text-color, #fff); + --tg-theme-secondary-bg-color: var(--tg-theme-secondary-bg-color, #16213e); + + --color-bg: var(--tg-theme-bg-color); + --color-text: var(--tg-theme-text-color); + --color-hint: var(--tg-theme-hint-color); + --color-link: var(--tg-theme-link-color); + --color-primary: var(--tg-theme-button-color); + --color-primary-text: var(--tg-theme-button-text-color); + --color-surface: var(--tg-theme-secondary-bg-color); + + --radius: 12px; + --radius-sm: 8px; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + background-color: var(--color-bg); + color: var(--color-text); + line-height: 1.5; + -webkit-font-smoothing: antialiased; + min-height: 100vh; + overflow-x: hidden; +} + +#root { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +a { + color: var(--color-link); + text-decoration: none; +} + +button { + cursor: pointer; + border: none; + outline: none; + font-family: inherit; +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..4996317 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,16 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import App from './App' +import './index.css' + +// Initialize Telegram Web App +if (window.Telegram?.WebApp) { + window.Telegram.WebApp.ready() + window.Telegram.WebApp.expand() +} + +createRoot(document.getElementById('root')!).render( + + + +) diff --git a/src/pages/HistoryPage.module.css b/src/pages/HistoryPage.module.css new file mode 100644 index 0000000..4fc3cb4 --- /dev/null +++ b/src/pages/HistoryPage.module.css @@ -0,0 +1,93 @@ +.page { + padding: 20px; + min-height: 100vh; +} + +.title { + font-size: 24px; + font-weight: 700; + margin-bottom: 8px; +} + +.subtitle { + color: var(--color-hint); + font-size: 14px; + margin-bottom: 24px; +} + +.empty { + color: var(--color-hint); + font-size: 15px; + text-align: center; + margin-top: 40px; +} + +.questCard { + display: block; + padding: 16px; + background: var(--color-surface); + border-radius: var(--radius); + margin-bottom: 12px; + text-decoration: none; + color: var(--color-text); +} + +.questCard h3 { + font-size: 16px; + font-weight: 600; + margin-bottom: 4px; +} + +.questCard p { + color: var(--color-hint); + font-size: 13px; +} + +.date { + font-size: 12px; + color: var(--color-hint); +} + +.daySection { + margin-bottom: 24px; +} + +.dayTitle { + font-size: 16px; + font-weight: 600; + margin-bottom: 12px; + color: var(--color-primary); +} + +.pointCard { + display: flex; + gap: 12px; + padding: 12px; + background: var(--color-surface); + border-radius: var(--radius-sm); + margin-bottom: 8px; +} + +.pointStatus { + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + font-weight: 600; + color: var(--color-primary); + flex-shrink: 0; +} + +.pointTitle { + font-size: 14px; + font-weight: 600; + margin-bottom: 4px; +} + +.pointContent { + font-size: 13px; + color: var(--color-hint); + line-height: 1.4; +} diff --git a/src/pages/HistoryPage.tsx b/src/pages/HistoryPage.tsx new file mode 100644 index 0000000..fa63761 --- /dev/null +++ b/src/pages/HistoryPage.tsx @@ -0,0 +1,106 @@ +import { useEffect, useState } from 'react' +import { useParams } from 'react-router-dom' +import styles from './HistoryPage.module.css' +import { fetchQuestHistory, fetchQuestDetail } from '@/utils/api' +import { Loader, ErrorState } from '@/components/ui' +import { getLanguage } from '@/utils/telegram' +import type { QuestHistoryItem } from '@/types' + +export function HistoryPage() { + const { questId } = useParams() + const lang = getLanguage() + const [quests, setQuests] = useState([]) + const [detail, setDetail] = useState(null) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + async function load() { + setIsLoading(true) + try { + if (questId) { + const data = await fetchQuestDetail(questId) + setDetail(data) + } else { + const data = await fetchQuestHistory() + setQuests(data.quests) + } + } catch (err: any) { + setError(err.message) + } finally { + setIsLoading(false) + } + } + load() + }, [questId]) + + if (isLoading) { + return + } + + if (error) { + return + } + + if (questId && detail) { + return ( +
+

{detail.quest.title || detail.quest.city_name}

+

+ {detail.quest.city_name}, {detail.quest.city_country} +

+ + {detail.days.map((day: any) => ( +
+

+ {lang === 'ru' ? `День ${day.day_number}` : `Day ${day.day_number}`} + {day.theme && ` — ${day.theme}`} +

+ {day.points.map((point: any) => ( +
+
+ {point.status === 'completed' ? 'V' : point.status === 'skipped' ? '-' : 'o'} +
+
+

{point.title}

+ {point.content_text && ( +

+ {point.content_text.substring(0, 100)}... +

+ )} +
+
+ ))} +
+ ))} +
+ ) + } + + // Quest list + return ( +
+

+ {lang === 'ru' ? 'Мои квесты' : 'My Quests'} +

+ + {quests.length === 0 ? ( +

+ {lang === 'ru' ? 'Пока нет завершённых квестов.' : 'No completed quests yet.'} +

+ ) : ( + quests.map((quest) => ( + +

{quest.title || quest.city_name}

+

{quest.city_name}, {quest.city_country}

+ + {quest.completed_at + ? new Date(quest.completed_at).toLocaleDateString() + : ''} + +
+ )) + )} +
+ ) +} diff --git a/src/pages/QuestPage.module.css b/src/pages/QuestPage.module.css new file mode 100644 index 0000000..0346856 --- /dev/null +++ b/src/pages/QuestPage.module.css @@ -0,0 +1,52 @@ +.page { + min-height: 100vh; + padding-bottom: 120px; +} + +.header { + padding: 20px 20px 8px; +} + +.questTitle { + font-size: 18px; + font-weight: 600; + color: var(--color-hint); +} + +.contentSection { + margin-top: 16px; +} + +.contentLoading { + margin-top: 16px; + padding: 20px; +} + +.actionsSpace { + height: 100px; +} + +.empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + padding: 40px 20px; + text-align: center; + gap: 12px; +} + +.empty h2 { + font-size: 20px; + font-weight: 600; +} + +.empty p { + color: var(--color-hint); + font-size: 15px; +} + +.waiting { + padding: 40px 20px; +} diff --git a/src/pages/QuestPage.tsx b/src/pages/QuestPage.tsx new file mode 100644 index 0000000..7150859 --- /dev/null +++ b/src/pages/QuestPage.tsx @@ -0,0 +1,107 @@ +import { useEffect } from 'react' +import styles from './QuestPage.module.css' +import { useQuestStore } from '@/store/questStore' +import { StepCard, StepProgress, ActionButtons, ContentView } from '@/components/quest' +import { Loader, ErrorState } from '@/components/ui' +import { getLanguage } from '@/utils/telegram' + +export function QuestPage() { + const { state, isLoading, error, loadCurrentQuest, sendEvent } = useQuestStore() + const lang = getLanguage() + + useEffect(() => { + loadCurrentQuest() + }, [loadCurrentQuest]) + + // Periodically reload to pick up bot-generated content + useEffect(() => { + if (state?.point?.arrived_at && !state.point.content_text) { + const interval = setInterval(() => { + loadCurrentQuest() + }, 3000) + return () => clearInterval(interval) + } + }, [state?.point?.arrived_at, state?.point?.content_text, loadCurrentQuest]) + + if (isLoading && !state) { + return + } + + if (error) { + return + } + + if (!state || !state.quest) { + return ( +
+

{lang === 'ru' ? 'Нет активного квеста' : 'No active quest'}

+

+ {lang === 'ru' + ? 'Напиши боту в чат, чтобы создать новый квест!' + : 'Message the bot in chat to create a new quest!'} +

+
+ ) + } + + const { quest, day, point, dayProgress } = state + const hasArrived = !!point?.arrived_at + const hasContent = !!point?.content_text + + return ( +
+ {/* Header */} +
+

{quest.title || quest.city_name}

+
+ + {/* Progress */} + {day && dayProgress && ( + + )} + + {/* Current step */} + {point ? ( + <> + + + {/* Content after arrival */} + {hasArrived && hasContent && ( +
+ +
+ )} + + {hasArrived && !hasContent && ( +
+ +
+ )} + + {/* Action buttons */} +
+ sendEvent('arrived')} + onCompleted={() => sendEvent('point_completed')} + onSkipped={() => sendEvent('skipped')} + onFinishedToday={() => sendEvent('finished_today')} + /> + + ) : ( +
+ +
+ )} +
+ ) +} diff --git a/src/store/questStore.ts b/src/store/questStore.ts new file mode 100644 index 0000000..c0140e6 --- /dev/null +++ b/src/store/questStore.ts @@ -0,0 +1,90 @@ +import { create } from 'zustand' +import type { QuestState, QuestPoint } from '../types' +import { fetchCurrentQuest, sendQuestEvent } from '../utils/api' + +interface QuestStore { + state: QuestState | null + isLoading: boolean + error: string | null + showContent: boolean + + loadCurrentQuest: () => Promise + sendEvent: (eventType: string, payload?: Record) => Promise + setShowContent: (show: boolean) => void + updatePoint: (point: QuestPoint) => void + reset: () => void +} + +export const useQuestStore = create((set, get) => ({ + state: null, + isLoading: false, + error: null, + showContent: false, + + loadCurrentQuest: async () => { + set({ isLoading: true, error: null }) + try { + const state = await fetchCurrentQuest() + const showContent = state.point?.arrived_at !== null && state.point?.content_text !== null + set({ state, isLoading: false, showContent }) + } catch (error: any) { + if (error.message?.includes('NO_ACTIVE_QUEST') || error.message?.includes('User not found')) { + set({ state: null, isLoading: false, error: null }) + } else { + set({ isLoading: false, error: error.message }) + } + } + }, + + sendEvent: async (eventType: string, payload?: Record) => { + const state = get().state + if (!state?.quest) return 'wait' + + try { + const response = await sendQuestEvent({ + questId: state.quest.id, + pointId: state.point?.id, + eventType, + payload, + }) + + if (response.updatedPoint) { + set((s) => ({ + state: s.state ? { + ...s.state, + point: response.updatedPoint || s.state.point, + } : null, + })) + } + + // Handle next actions + if (response.nextAction === 'show_content') { + // Reload to get full content (generated by bot) + setTimeout(() => get().loadCurrentQuest(), 2000) + set({ showContent: true }) + } else if (response.nextAction === 'next_point') { + set({ showContent: false }) + // Wait for bot to generate next point, then reload + setTimeout(() => get().loadCurrentQuest(), 3000) + } else if (response.nextAction === 'day_complete' || response.nextAction === 'quest_complete') { + set({ showContent: false }) + setTimeout(() => get().loadCurrentQuest(), 2000) + } + + return response.nextAction + } catch (error: any) { + console.error('Failed to send event:', error) + return 'wait' + } + }, + + setShowContent: (show: boolean) => set({ showContent: show }), + + updatePoint: (point: QuestPoint) => { + set((s) => ({ + state: s.state ? { ...s.state, point } : null, + })) + }, + + reset: () => set({ state: null, isLoading: false, error: null, showContent: false }), +})) diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..b7db98d --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,60 @@ +export interface QuestInfo { + id: string + title: string | null + city_name: string + city_country: string + number_of_days: number + pace: string + companions: string + status: string +} + +export interface QuestDay { + id: string + quest_id: string + day_number: number + theme: string | null + status: string + started_at: string | null + completed_at: string | null +} + +export interface QuestPoint { + id: string + day_id: string + title: string + location_lat: number | null + location_lon: number | null + teaser_text: string | null + content_text: string | null + order_in_day: number + status: string + arrived_at: string | null + completed_at: string | null +} + +export interface QuestState { + quest: QuestInfo + day: QuestDay | null + point: QuestPoint | null + dayProgress: { + totalPoints: number + completedPoints: number + } | null +} + +export interface EventResponse { + success: boolean + updatedPoint: QuestPoint | null + nextAction: 'wait' | 'show_content' | 'next_point' | 'day_complete' | 'quest_complete' | 'close_app' +} + +export interface QuestHistoryItem { + id: string + title: string | null + city_name: string + city_country: string + number_of_days: number + status: string + completed_at: string | null +} diff --git a/src/utils/api.ts b/src/utils/api.ts new file mode 100644 index 0000000..971f778 --- /dev/null +++ b/src/utils/api.ts @@ -0,0 +1,48 @@ +import { getTelegramInitData } from './telegram' +import type { QuestState, EventResponse } from '../types' + +const API_BASE = '/api' + +async function apiFetch(path: string, options: RequestInit = {}): Promise { + const initData = getTelegramInitData() + + const response = await fetch(`${API_BASE}${path}`, { + ...options, + headers: { + 'Content-Type': 'application/json', + 'X-Telegram-Init-Data': initData, + ...options.headers, + }, + }) + + if (!response.ok) { + const error = await response.json().catch(() => ({ error: 'Unknown error' })) + throw new Error(error.error || `HTTP ${response.status}`) + } + + return response.json() +} + +export async function fetchCurrentQuest(): Promise { + return apiFetch('/quest/current') +} + +export async function sendQuestEvent(data: { + questId: string + pointId?: string + eventType: string + payload?: Record +}): Promise { + return apiFetch('/quest/events', { + method: 'POST', + body: JSON.stringify(data), + }) +} + +export async function fetchQuestHistory() { + return apiFetch<{ quests: any[] }>('/quest/history') +} + +export async function fetchQuestDetail(questId: string) { + return apiFetch<{ quest: any; days: any[] }>(`/quest/history/${questId}`) +} diff --git a/src/utils/telegram.ts b/src/utils/telegram.ts new file mode 100644 index 0000000..7b4c71b --- /dev/null +++ b/src/utils/telegram.ts @@ -0,0 +1,24 @@ +export function getTelegramWebApp() { + return window.Telegram?.WebApp +} + +export function getTelegramUser() { + return window.Telegram?.WebApp?.initDataUnsafe?.user +} + +export function getTelegramInitData(): string { + return window.Telegram?.WebApp?.initData || '' +} + +export function getLanguage(): string { + const langCode = getTelegramUser()?.language_code + if (!langCode) return 'ru' + if (langCode.startsWith('ru') || langCode.startsWith('uk') || langCode.startsWith('be')) { + return 'ru' + } + return 'en' +} + +export function closeMiniApp() { + window.Telegram?.WebApp?.close() +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..ddf3fe6 --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,56 @@ +/// + +interface TelegramWebApp { + ready(): void + expand(): void + close(): void + initData: string + initDataUnsafe: { + user?: { + id: number + first_name: string + last_name?: string + username?: string + language_code?: string + } + auth_date: number + hash: string + } + themeParams: { + bg_color?: string + text_color?: string + hint_color?: string + link_color?: string + button_color?: string + button_text_color?: string + secondary_bg_color?: string + } + colorScheme: 'light' | 'dark' + MainButton: { + text: string + color: string + textColor: string + isVisible: boolean + isActive: boolean + show(): void + hide(): void + onClick(callback: () => void): void + offClick(callback: () => void): void + setText(text: string): void + enable(): void + disable(): void + } + BackButton: { + isVisible: boolean + show(): void + hide(): void + onClick(callback: () => void): void + offClick(callback: () => void): void + } +} + +interface Window { + Telegram?: { + WebApp?: TelegramWebApp + } +} diff --git a/tsconfig.bot.json b/tsconfig.bot.json new file mode 100644 index 0000000..2d9fba0 --- /dev/null +++ b/tsconfig.bot.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020"], + "outDir": "./dist-bot", + "rootDir": ".", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": false, + "noEmitOnError": false, + "resolveJsonModule": true + }, + "include": ["bot/**/*"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5b0eca4 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src"] +} diff --git a/tsconfig.server.json b/tsconfig.server.json new file mode 100644 index 0000000..e16e0d5 --- /dev/null +++ b/tsconfig.server.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020"], + "outDir": "./dist-server", + "rootDir": ".", + "strict": false, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": false, + "noEmitOnError": false + }, + "include": ["api/**/*", "server/**/*", "bot/**/*"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..21ccdfa --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import path from 'path' + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + server: { + host: true, + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true, + }, + }, + }, +})