From 97fb289fe7ead543c6f3fa98fede8c0556efe5d6 Mon Sep 17 00:00:00 2001 From: szbk Date: Sat, 28 Feb 2026 02:44:41 +0300 Subject: [PATCH] first commit --- .dockerignore | 52 + .env.example | 41 + .gitignore | 37 + Dockerfile | 86 + Dockerfile.dev | 36 + README.md | 171 + SKILL.MD | 133 + doc/api.md | 256 ++ doc/decisions/ADR-001-scraper-choice.md | 53 + doc/decisions/ADR-002-cache-strategy.md | 61 + doc/decisions/ADR-003-api-key-strategy.md | 63 + doc/ops.md | 291 ++ doc/overview.md | 82 + doc/socket-events.md | 256 ++ docker-compose.dev.yml | 95 + docker-compose.yml | 94 + .../2026-02-27-db-content-list-brainstorm.md | 29 + frontend/.gitignore | 24 + frontend/README.md | 73 + frontend/eslint.config.js | 23 + frontend/index.html | 13 + frontend/package-lock.json | 3865 +++++++++++++++++ frontend/package.json | 45 + frontend/postcss.config.cjs | 14 + frontend/public/netflix.png | Bin 0 -> 5668 bytes frontend/public/vite.svg | 1 + frontend/src/App.css | 2 + frontend/src/App.tsx | 8 + frontend/src/assets/react.svg | 1 + frontend/src/index.css | 10 + frontend/src/main.tsx | 19 + frontend/src/pages/MoviesPage.tsx | 208 + frontend/tsconfig.app.json | 28 + frontend/tsconfig.json | 7 + frontend/tsconfig.node.json | 26 + frontend/vite.config.ts | 21 + package-lock.json | 2301 ++++++++++ package.json | 37 + .../20250227000000_init/migration.sql | 95 + .../migration.sql | 2 + .../migration.sql | 5 + .../migration.sql | 5 + prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 103 + prisma/seed.ts | 49 + scripts/dev-startup.sh | 34 + scripts/startup.sh | 30 + skills/brainstorming/SKILL.md | 104 + skills/brainstorming/agents/openai.yaml | 4 + src/config/database.ts | 63 + src/config/env.ts | 77 + src/config/redis.ts | 66 + src/config/socket.ts | 131 + src/index.ts | 112 + src/middleware/auth.middleware.ts | 73 + src/middleware/error.middleware.ts | 50 + src/middleware/rateLimit.middleware.ts | 87 + src/middleware/validation.middleware.ts | 93 + src/routes/api.routes.ts | 234 + src/routes/health.routes.ts | 54 + src/routes/tmdb.routes.ts | 222 + src/services/cache.service.ts | 146 + src/services/content.service.ts | 239 + src/services/job.service.ts | 237 + src/services/scraper.service.ts | 284 ++ src/services/tmdb.service.ts | 429 ++ src/types/index.ts | 210 + src/utils/logger.ts | 97 + tsconfig.json | 27 + tsconfig.tsbuildinfo | 1 + 70 files changed, 11928 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Dockerfile.dev create mode 100644 README.md create mode 100644 SKILL.MD create mode 100644 doc/api.md create mode 100644 doc/decisions/ADR-001-scraper-choice.md create mode 100644 doc/decisions/ADR-002-cache-strategy.md create mode 100644 doc/decisions/ADR-003-api-key-strategy.md create mode 100644 doc/ops.md create mode 100644 doc/overview.md create mode 100644 doc/socket-events.md create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.yml create mode 100644 docs/brainstorms/2026-02-27-db-content-list-brainstorm.md create mode 100644 frontend/.gitignore create mode 100644 frontend/README.md create mode 100644 frontend/eslint.config.js create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.cjs create mode 100644 frontend/public/netflix.png create mode 100644 frontend/public/vite.svg create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/assets/react.svg create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/pages/MoviesPage.tsx create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 prisma/migrations/20250227000000_init/migration.sql create mode 100644 prisma/migrations/20250227000001_add_age_rating/migration.sql create mode 100644 prisma/migrations/20250227000002_add_content_type/migration.sql create mode 100644 prisma/migrations/20250227000003_add_current_season/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/schema.prisma create mode 100644 prisma/seed.ts create mode 100755 scripts/dev-startup.sh create mode 100755 scripts/startup.sh create mode 100644 skills/brainstorming/SKILL.md create mode 100644 skills/brainstorming/agents/openai.yaml create mode 100644 src/config/database.ts create mode 100644 src/config/env.ts create mode 100644 src/config/redis.ts create mode 100644 src/config/socket.ts create mode 100644 src/index.ts create mode 100644 src/middleware/auth.middleware.ts create mode 100644 src/middleware/error.middleware.ts create mode 100644 src/middleware/rateLimit.middleware.ts create mode 100644 src/middleware/validation.middleware.ts create mode 100644 src/routes/api.routes.ts create mode 100644 src/routes/health.routes.ts create mode 100644 src/routes/tmdb.routes.ts create mode 100644 src/services/cache.service.ts create mode 100644 src/services/content.service.ts create mode 100644 src/services/job.service.ts create mode 100644 src/services/scraper.service.ts create mode 100644 src/services/tmdb.service.ts create mode 100644 src/types/index.ts create mode 100644 src/utils/logger.ts create mode 100644 tsconfig.json create mode 100644 tsconfig.tsbuildinfo diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d8410b4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,52 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build output +dist/ +build/ + +# Environment files +.env +.env.local +.env.*.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Git +.git/ +.gitignore + +# Docker +Dockerfile* +docker-compose*.yml +.dockerignore + +# Documentation +doc/ +*.md +!README.md + +# Prisma +prisma/migrations/*_* + +# Test +coverage/ +.nyc_output/ + +# Misc +*.log +*.pid +*.seed +*.pid.lock diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..764b713 --- /dev/null +++ b/.env.example @@ -0,0 +1,41 @@ +# =========================================== +# Netflix Scraper API - Environment Variables +# Copy this file to .env and fill in the values +# =========================================== + +# === Server Configuration === +PORT=3000 +NODE_ENV=development + +# === PostgreSQL Configuration === +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +POSTGRES_USER=postgres +POSTGRES_PASSWORD=your-secure-password-here +POSTGRES_DB=netflix_scraper + +# === Redis Configuration === +REDIS_HOST=localhost +REDIS_PORT=6379 +# Cache TTL in seconds (default: 7 days = 604800) +REDIS_TTL_SECONDS=604800 + +# === Rate Limiting Configuration === +# Time window in milliseconds (default: 1 minute) +RATE_LIMIT_WINDOW_MS=60000 +# Maximum requests per window per IP +RATE_LIMIT_MAX_REQUESTS=30 + +# === API Keys (for frontend authentication) === +# Generate secure random keys for production! +API_KEY_WEB=web-frontend-key-change-me-in-production +API_KEY_MOBILE=mobile-app-key-change-me-in-production +API_KEY_ADMIN=admin-key-super-secret-change-me + +# === TMDB API Configuration === +# Get your API key from https://www.themoviedb.org/settings/api +TMDB_API_KEY=your-tmdb-api-key-here +TMDB_ACCESS_TOKEN=your-tmdb-access-token-here + +# === Optional: Logging === +# LOG_LEVEL=info # debug, info, warn, error diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..344b7ab --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# Environment files +.env +.env.local +.env.*.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Test coverage +coverage/ +.nyc_output/ + +# Misc +*.pid +*.seed +*.pid.lock diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..37721eb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,86 @@ +# =========================================== +# Stage 1: Dependencies +# =========================================== +FROM node:20-alpine AS deps + +WORKDIR /app + +# Install libc6-compat and openssl for Prisma +RUN apk add --no-cache libc6-compat openssl + +# Set dummy DATABASE_URL for Prisma generate (doesn't need real connection) +ENV DATABASE_URL="postgresql://dummy:dummy@dummy:5432/dummy" + +# Copy package files +COPY package.json package-lock.json* ./ +COPY prisma ./prisma/ + +# Install dependencies +RUN npm ci --include=dev + +# Generate Prisma client +RUN npx prisma generate + +# =========================================== +# Stage 2: Builder +# =========================================== +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy dependencies from deps stage +COPY --from=deps /app/node_modules ./node_modules +COPY --from=deps /app/prisma ./prisma + +# Copy source files +COPY . . + +# Build TypeScript +RUN npm run build + +# Prune dev dependencies +RUN npm prune --production + +# =========================================== +# Stage 3: Production Runner +# =========================================== +FROM node:20-alpine AS runner + +WORKDIR /app + +# Install libc6-compat and openssl for Prisma (busybox includes netcat) +RUN apk add --no-cache libc6-compat openssl + +# Create non-root user for security +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 appuser + +# Set environment +ENV NODE_ENV=production +ENV PORT=3000 + +# Copy built files and dependencies +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/prisma ./prisma +COPY --from=builder /app/package.json ./ +COPY scripts/startup.sh ./scripts/startup.sh + +# Set ownership +RUN chown -R appuser:nodejs /app + +# Make startup script executable +RUN chmod +x ./scripts/startup.sh + +# Switch to non-root user +USER appuser + +# Expose port +EXPOSE 3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 + +# Start application with startup script +CMD ["sh", "./scripts/startup.sh"] diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..86ad232 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,36 @@ +# =========================================== +# Development Dockerfile with Hot Reload +# =========================================== +FROM node:20-alpine + +WORKDIR /app + +# Install dependencies for development (Prisma needs libc6-compat and openssl) +RUN apk add --no-cache libc6-compat openssl + +# Set environment +ENV NODE_ENV=development +ENV PORT=3000 + +# Set dummy DATABASE_URL for Prisma generate (doesn't need real connection) +ENV DATABASE_URL="postgresql://dummy:dummy@dummy:5432/dummy" + +# Copy package files +COPY package.json package-lock.json* ./ +COPY prisma ./prisma/ + +# Install all dependencies (including dev) +RUN npm install + +# Generate Prisma client +RUN npx prisma generate + +# Copy startup script +COPY scripts/dev-startup.sh ./scripts/dev-startup.sh +RUN chmod +x ./scripts/dev-startup.sh + +# Expose port +EXPOSE 3000 + +# Start with dev startup script (includes migrations) +CMD ["sh", "./scripts/dev-startup.sh"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..6bfb2db --- /dev/null +++ b/README.md @@ -0,0 +1,171 @@ +# Netflix Scraper API + +Netflix içerik sayfalarından film/dizi bilgilerini çeken yüksek performanslı bir backend API servisi. + +## Özellikler + +- **Scraping**: Netflix URL'lerinden otomatik içerik bilgisi çekme +- **Cache**: Redis ile 7 günlük önbellek (yapılandırılabilir) +- **Kalıcılık**: PostgreSQL ile verilerin kalıcı saklanması +- **Real-time**: Socket.IO ile canlı ilerleme bildirimleri +- **Güvenlik**: API Key authentication ve rate limiting +- **Docker**: Tek komut ile ayağa kalkma + +## Hızlı Başlangıç + +```bash +# .env dosyasını oluştur +cp .env.example .env + +# API key'leri düzenle +nano .env + +# Başlat (tek komut!) +docker compose -f docker-compose.dev.yml up --build +``` + +API şu adreste çalışacak: `http://localhost:3000` + +## API Kullanımı + +### İstek Örneği + +```bash +curl -X POST http://localhost:3000/api/getinfo \ + -H "Content-Type: application/json" \ + -H "X-API-Key: web-dev-key-change-me" \ + -d '{"url": "https://www.netflix.com/tr/title/81616256"}' +``` + +### Yanıt Örneği + +```json +{ + "success": true, + "data": { + "title": "Hayata Röveşata Çeken Adam", + "year": 2022, + "plot": "Dünyaya karşı duyduğu öfke...", + "genres": ["18+", "Komedi"], + "cast": ["Tom Hanks", "Mariana Treviño", "Rachel Keller"], + "backdrop": "https://occ-0-7335-..." + } +} +``` + +## Endpoints + +| Method | Endpoint | Açıklama | +|--------|----------|----------| +| `GET` | `/health` | Sağlık kontrolü | +| `GET` | `/ready` | Bağımlılık kontrolü | +| `POST` | `/api/getinfo` | İçerik bilgisi getir | +| `POST` | `/api/getinfo/async` | Asenkron job oluştur | +| `GET` | `/api/jobs/:jobId` | Job durumu sorgula | + +## Socket.IO Events + +| Event | Yön | Açıklama | +|-------|-----|----------| +| `job:subscribe` | Client → Server | Job'a abone ol | +| `job:progress` | Server → Client | İlerleme güncellemesi | +| `job:completed` | Server → Client | İşlem tamamlandı | +| `job:error` | Server → Client | Hata oluştu | + +## Environment Değişkenleri + +| Değişken | Açıklama | Varsayılan | +|----------|----------|------------| +| `PORT` | Sunucu portu | `3000` | +| `NODE_ENV` | Ortam | `development` | +| `POSTGRES_*` | PostgreSQL ayarları | - | +| `REDIS_*` | Redis ayarları | - | +| `REDIS_TTL_SECONDS` | Cache süresi | `604800` (7 gün) | +| `RATE_LIMIT_*` | Rate limit ayarları | - | +| `API_KEY_*` | API anahtarları | - | + +## Migration + +Migration'lar otomatik olarak container başlatılırken çalışır. + +```bash +# Manuel migration +docker compose exec app npx prisma migrate deploy +``` + +## Teknoloji Yığını + +| Katman | Teknoloji | +|--------|-----------| +| Runtime | Node.js 20+ | +| Framework | Express.js | +| Database | PostgreSQL 16 | +| Cache | Redis 7 | +| Real-time | Socket.IO 4 | +| Scraper | Cheerio | +| ORM | Prisma | + +## Proje Yapısı + +``` +. +├── src/ +│ ├── config/ # Yapılandırma (env, database, redis, socket) +│ ├── middleware/ # Express middleware (auth, rate-limit, validation) +│ ├── routes/ # API rotaları +│ ├── services/ # İş mantığı (scraper, cache, content, job) +│ ├── types/ # TypeScript tipleri +│ └── utils/ # Yardımcı fonksiyonlar +├── prisma/ +│ ├── schema.prisma # Veritabanı şeması +│ └── seed.ts # Başlangıç verileri +├── doc/ +│ ├── overview.md # Proje özeti +│ ├── api.md # API dokümantasyonu +│ ├── ops.md # Operasyon notları +│ └── socket-events.md # Socket.IO events +├── docker-compose.dev.yml +├── docker-compose.yml +├── Dockerfile +└── package.json +``` + +## Dokümantasyon + +- **Proje Özeti**: [`doc/overview.md`](doc/overview.md) +- **API Dokümantasyonu**: [`doc/api.md`](doc/api.md) +- **Operasyon**: [`doc/ops.md`](doc/ops.md) +- **Socket Events**: [`doc/socket-events.md`](doc/socket-events.md) + +## Geliştirme + +### Local Development + +```bash +# Bağımlılıkları yükle +npm install + +# Prisma client oluştur +npx prisma generate + +# Development modda çalıştır +npm run dev +``` + +### Production Build + +```bash +npm run build +npm start +``` + +## Güvenlik + +- **API Key Authentication**: Tüm istekler API key gerektirir +- **Rate Limiting**: Dakikada max 30 istek (yapılandırılabilir) +- **Non-root Container**: Production container'ları root olmayan kullanıcı ile çalışır +- **Input Validation**: Tüm girdiler Zod ile doğrulanır + +## Lisans + +MIT diff --git a/SKILL.MD b/SKILL.MD new file mode 100644 index 0000000..5fb1e78 --- /dev/null +++ b/SKILL.MD @@ -0,0 +1,133 @@ +# AI Project Skills (MUST APPLY) + +Bu doküman, projeyi geliştirirken AI agent’ın (Claude) UYGULAMAK ZORUNDA olduğu kuralları tanımlar. +Buradaki her madde “MUST” seviyesindedir. İstisna yoktur. +Bir değişiklik bu kuralları etkiliyorsa aynı değişiklikte dokümanlar da güncellenmelidir. + +--- + +## 1) Docker & Compose Standartları (MUST) + +- Projede Docker altyapısı kullanılacak. +- İki compose dosyası olacak: + - `docker-compose.dev.yml` (dev) + - `docker-compose.yml` (prod) +- Dev ortamında yapılan değişiklikler canlı yansıyacak (hot reload / watch). +- Proje DEV ortamında **tek komut** ile ayağa kalkmalıdır: + - `docker compose -f docker-compose.dev.yml up --build` +- Bu komut dışında: + - manuel migrate/seed/kurulum komutu çalıştırmak + - ekstra adım istemek + - “önce şunu yap sonra bunu yap” tarzı süreçler + **KABUL EDİLMEZ.** +- Servisler için healthcheck tanımlanacak (db/redis/app) ve bağımlılıklar healthcheck üzerinden yönetilecek. +- Docker image’ları mümkünse multi-stage build ile üretilecek; prod image minimal olacak; container non-root user ile çalışacak. + +--- + +## 2) Ortam Değişkenleri (MUST) + +- Ortam değişkenleri `.env` üzerinden tanımlanacak. +- Repo içinde `.env` bulunmayacak (gitignore). +- `.env.example` dosyası oluşturulacak. +- `.env`yi etkileyen her güncellemede `.env.example` da aynı PR/commit içinde güncellenecek. +- Compose dosyalarında: + - `.env` dosyası okunacak ve değişkenler servislere aktarılacak. + - `.env` içinde olmayan bir değişken kullanılıyorsa compose içinde DEFAULT değer verilecek (örn. `${VAR:-default}`). +- Uygulama ayağa kalkarken env doğrulaması yapılacak: + - Eksik değişken varsa fail-fast (startup’ta hata). + - Tip doğrulaması yapılacak (string/number/boolean). + - Öneri: `zod` / `envalid` benzeri bir yaklaşım. + +--- + +## 3) Backend Teknoloji Yığını (Koşullu MUST) + +Backend kullanılacaksa aşağıdakiler ZORUNLUDUR: +- Node.js +- Socket.IO +- Redis +- PostgreSQL + +Backend için ek zorunluluklar: +- `/health` ve `/ready` endpointleri olacak. +- Structured logging (JSON) kullanılacak, log seviyeleri (debug/info/warn/error) standardize edilecek. +- Hata yanıt formatı tutarlı olacak (error code + message + details). + +--- + +## 4) PostgreSQL Şema Yönetimi (MUST) — Tek Komut Kuralı ile Uyumlu + +- PostgreSQL kullanılıyorsa şema yönetimi **deterministic** olmalıdır: + - Temiz kurulumda ve güncellemelerde aynı sonuca güvenilir şekilde ulaşmalıdır. +- Migration yaklaşımı kullanılacaksa: + - Migration’lar **backend container startup adımının parçası** olarak otomatik çalıştırılacaktır. + - Kullanıcıdan manuel olarak `migrate/seed` komutu çalıştırması istenmeyecektir. + - Backend, DB hazır olana kadar bekleyecek (healthcheck + retry/backoff) ve ardından: + 1) migration’ı çalıştıracak + 2) uygulamayı başlatacaktır +- DB migration aracı seçilebilir (Prisma/Knex/TypeORM/Flyway vb.) ancak seçilen araç: + - README’de ve `/doc/ops.md` içinde açıkça belirtilecek + - dev/prod için aynı “tek komut” felsefesini bozmayacak şekilde entegre edilecektir. +- Opsiyonel seed gerekiyorsa (ör. admin user vb.): + - seed işlemi de otomatik olacak + - yine manuel komut gerektirmeyecek. + +--- + +## 5) Frontend Teknoloji Yığını (Koşullu MUST) + +Frontend kullanılacaksa aşağıdakiler ZORUNLUDUR: +- React +- React Router +- Font Awesome + +Frontend için ek zorunluluklar: +- Socket ile gelen canlı veriler ve state yönetimi README’de ve `/doc/socket-events.md` içinde açıklanacak. +- Uygulama konfigürasyonu (API base url, socket url vb.) `.env` üzerinden yönetilecek. + +--- + +## 6) Dokümantasyon Zorunlulukları (MUST) + +- Projedeki tüm dökümanlar `/doc` dizini altında olacak. +- Proje için mutlaka bir “özet dokümanı” hazırlanacak: + - `/doc/overview.md` + - İçerik: Projenin ne yaptığı, neden oluşturulduğu, nasıl ilerlediği, önemli kararlar, güncellemeler. + - Projeye yapılan her güncellemede bu dosya gerektiği kadar güncellenecek. +- API ve Socket sözleşmeleri dokümante edilecek: + - REST varsa: `/doc/api.md` (OpenAPI/Swagger referansı dahil) + - Socket varsa: `/doc/socket-events.md` (event adı, yön, payload şeması, örnek) +- Operasyon / çalıştırma notları: `/doc/ops.md` (build, run, dev/prod, troubleshooting). +- Mimari kararlar için `/doc/decisions/ADR-XXXX.md` formatı kullanılacak (kısa ve net). + +--- + +## 7) README Standartları (MUST) + +- README her zaman güncel kalacak. +- Projeye ait frontend ve backend bilgileri detaylı anlatılacak: + - Kurulum + - Dev/Prod çalıştırma + - Env değişkenleri + - Endpointlerin ne iş yaptığı + - Socket ile iletilen canlı veriler (event listesi) + - Migration/seed yaklaşımı (varsa) ve “tek komut” akışının açıklaması +- README içinde anlatım Font Awesome ikonları ile zenginleştirilecek. +- README, `/doc` içindeki ana dokümanlara link verecek. + +--- + +## 8) Değişiklik Kuralı (MUST) + +- Bir değişiklik: + - `.env` / `.env.example`i etkiliyorsa ikisi birlikte güncellenecek. + - API/Socket sözleşmesini etkiliyorsa `/doc/api.md` veya `/doc/socket-events.md` güncellenecek. + - Çalıştırma şeklini (docker/komut/akış) etkiliyorsa README ve `/doc/ops.md` güncellenecek. + - DB şemasını etkiliyorsa migration/init/ops dokümanları aynı değişiklikte güncellenecek. +- Bu güncellemeler yapılmadan değişiklik “tamamlandı” sayılmaz. + +--- + +## 9) İletişim Kuralı (MUST) +- Proje oluşturulurken ve geliştirilirken tüm iletişim Türkçe yapılacak. \ No newline at end of file diff --git a/doc/api.md b/doc/api.md new file mode 100644 index 0000000..4c79f01 --- /dev/null +++ b/doc/api.md @@ -0,0 +1,256 @@ +# Netflix Scraper API - API Dokümantasyonu + +## Base URL + +``` +Development: http://localhost:3000 +Production: https://api.yourdomain.com +``` + +## Authentication + +Tüm API istekleri `X-API-Key` header'ı gerektirir. + +```http +X-API-Key: your-api-key-here +``` + +### API Key Tipleri + +| Key | Kullanım | +|-----|----------| +| `API_KEY_WEB` | Web frontend | +| `API_KEY_MOBILE` | Mobil uygulama | +| `API_KEY_ADMIN` | Admin panel | + +--- + +## Endpoints + +### POST /api/getinfo + +Netflix URL'sinden içerik bilgisi getirir. + +**Request** + +```http +POST /api/getinfo +Content-Type: application/json +X-API-Key: your-api-key + +{ + "url": "https://www.netflix.com/tr/title/81616256" +} +``` + +**Response (Success)** + +```json +{ + "success": true, + "data": { + "title": "Hayata Röveşata Çeken Adam", + "year": 2022, + "plot": "Dünyaya karşı duyduğu öfke ve yaşadığı kederin katılaştırdığı huysuz bir emekli, yaşamına son vermeyi planlar. Ancak hayatına neşeli bir genç aile girince tüm planları suya düşer.", + "genres": ["18+", "Komedi"], + "cast": ["Tom Hanks", "Mariana Treviño", "Rachel Keller"], + "backdrop": "https://occ-0-7335-3467.1.nflxso.net/dnm/api/v6/..." + } +} +``` + +**Response (Error)** + +```json +{ + "success": false, + "error": { + "code": "VALIDATION_ERROR", + "message": "Invalid request parameters", + "details": { + "errors": [ + { + "field": "url", + "message": "URL must be a valid Netflix title URL" + } + ] + } + } +} +``` + +**Status Codes** + +| Code | Açıklama | +|------|----------| +| 200 | Başarılı | +| 400 | Geçersiz istek | +| 401 | API key eksik | +| 403 | Geçersiz API key | +| 429 | Rate limit aşıldı | +| 500 | Sunucu hatası | + +--- + +### POST /api/getinfo/async + +Asenkron scraping job'u oluşturur. Büyük ölçekli kullanım için uygundur. + +**Request** + +```http +POST /api/getinfo/async +Content-Type: application/json +X-API-Key: your-api-key + +{ + "url": "https://www.netflix.com/tr/title/81616256" +} +``` + +**Response** + +```json +{ + "success": true, + "data": { + "jobId": "550e8400-e29b-41d4-a716-446655440000", + "status": "pending" + } +} +``` + +**Socket ile İzleme** + +Job durumunu Socket.IO ile izleyebilirsiniz: + +```javascript +socket.emit('job:subscribe', jobId); +socket.on('job:progress', (data) => console.log(data)); +socket.on('job:completed', (data) => console.log(data)); +socket.on('job:error', (data) => console.error(data)); +``` + +--- + +### GET /api/jobs/:jobId + +Job durumunu sorgular. + +**Request** + +```http +GET /api/jobs/550e8400-e29b-41d4-a716-446655440000 +X-API-Key: your-api-key +``` + +**Response** + +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "url": "https://www.netflix.com/tr/title/81616256", + "status": "completed", + "progress": 100, + "step": "completed", + "result": { + "title": "Hayata Röveşata Çeken Adam", + "year": 2022, + "plot": "...", + "genres": ["18+", "Komedi"], + "cast": ["Tom Hanks", "Mariana Treviño", "Rachel Keller"], + "backdrop": "https://..." + }, + "createdAt": "2025-02-27T10:00:00.000Z", + "updatedAt": "2025-02-27T10:00:05.000Z" + } +} +``` + +--- + +### GET /health + +Basit sağlık kontrolü. + +**Response** + +```json +{ + "status": "ok", + "timestamp": "2025-02-27T10:00:00.000Z", + "uptime": 3600 +} +``` + +--- + +### GET /ready + +Tüm bağımlılıkların hazır olup olmadığını kontrol eder. + +**Response** + +```json +{ + "status": "ready", + "timestamp": "2025-02-27T10:00:00.000Z", + "checks": { + "database": "healthy", + "redis": "healthy" + }, + "env": "production" +} +``` + +--- + +## Error Codes + +| Code | Açıklama | +|------|----------| +| `MISSING_API_KEY` | API key header'ı eksik | +| `INVALID_API_KEY` | Geçersiz API key | +| `VALIDATION_ERROR` | İstek parametreleri geçersiz | +| `RATE_LIMIT_EXCEEDED` | Genel rate limit aşıldı | +| `SCRAPE_RATE_LIMIT_EXCEEDED` | Scraping rate limit aşıldı | +| `SCRAPE_ERROR` | Netflix'ten veri çekilemedi | +| `JOB_NOT_FOUND` | Job bulunamadı | +| `INTERNAL_ERROR` | Beklenmeyen sunucu hatası | + +--- + +## Rate Limiting + +### Genel Rate Limit +- **Window**: 1 dakika +- **Max İstek**: 30 istek/dakika/IP+APIKey + +### Scraping Rate Limit +- **Window**: 1 dakika +- **Max İstek**: 10 istek/dakika/IP+APIKey + +Rate limit değerleri `.env` dosyasından yapılandırılabilir. + +--- + +## Request/Response Formatları + +### Content Data (GetInfoResponse) + +| Alan | Tip | Açıklama | +|------|-----|----------| +| `title` | string | İçerik başlığı | +| `year` | number \| null | Yayın yılı | +| `plot` | string \| null | Açıklama/özet | +| `genres` | string[] | Tür listesi | +| `cast` | string[] | Oyuncu listesi | +| `backdrop` | string \| null | Arka plan görseli URL | + +--- + +## OpenAPI / Swagger + +OpenAPI spesifikasyonu için: `/api/docs` (yakında) diff --git a/doc/decisions/ADR-001-scraper-choice.md b/doc/decisions/ADR-001-scraper-choice.md new file mode 100644 index 0000000..de7b91e --- /dev/null +++ b/doc/decisions/ADR-001-scraper-choice.md @@ -0,0 +1,53 @@ +# ADR-001: Cheerio Scraping Kütüphanesi Seçimi + +## Durum + +Kabul edildi + +## Bağlam + +Netflix içerik sayfalarından HTML parsing ile veri çekmemiz gerekiyor. İki ana seçenek var: + +1. **Cheerio**: Lightweight HTML parser +2. **Playwright/Puppeteer**: Headless browser automation + +## Karar + +**Cheerio** seçildi. + +## Gerekçe + +### Cheerio Avantajları +- Hafif ve hızlı +- Düşük kaynak kullanımı +- Basit API +- Daha az bağımlılık + +### Playwright Avantajları +- JavaScript rendering desteği +- Daha güçlü scraping +- Dinamik içerik desteği + +### Seçim Nedeni +1. Netflix sayfalarının HTML'inde temel veriler mevcut +2. Client-side rendering gerektiren kritik veri yok +3. Performans öncelikli +4. Başlangıç için Cheerio yeterli + +## Sonuçlar + +### Olumlu +- Düşük kaynak kullanımı +- Hızlı yanıt süresi +- Basit bakım + +### Olumsuz +- JavaScript rendering gerektiren sayfalar için çalışmayabilir +- Netflix client-side rendering'e geçerse güncelleme gerekir + +### Alternatif Plan +Eğer Cheerio yetersiz kalırsa Playwright'a geçiş yapılabilir. Altyapı buna uygun hazır. + +## Tarih + +2025-02-27 diff --git a/doc/decisions/ADR-002-cache-strategy.md b/doc/decisions/ADR-002-cache-strategy.md new file mode 100644 index 0000000..9d83a62 --- /dev/null +++ b/doc/decisions/ADR-002-cache-strategy.md @@ -0,0 +1,61 @@ +# ADR-002: Hybrid Cache Stratejisi + +## Durum + +Kabul edildi + +## Bağlam + +Netflix'ten scraped verileri nasıl saklayacağımıza karar vermemiz gerekiyor. Seçenekler: + +1. **Sadece Cache**: Redis'te TTL ile sakla +2. **Sadece Database**: PostgreSQL'de kalıcı sakla +3. **Hybrid**: Cache + Database birlikte + +## Karar + +**Hybrid Cache Stratejisi** (Cache → DB → Netflix) seçildi. + +## Akış + +``` +İstek → Redis Cache → Varsa dön + → Yoksa → PostgreSQL → Varsa dön, Cache'e yaz + → Yoksa → Netflix'ten çek → DB'ye yaz → Cache'e yaz → Dön +``` + +## Gerekçe + +### Sadece Cache Eksikleri +- TTL dolduğunda veri kaybı +- Yeniden scraping maliyeti + +### Sadece Database Eksikleri +- Her istekte DB sorgusu +- Daha yavaş yanıt + +### Hybrid Avantajları +1. **Hız**: Cache hit durumunda anlık yanıt +2. **Kalıcılık**: Veriler DB'de saklanır +3. **Verimlilik**: Netflix'e gereksiz istek atılmaz +4. **TTL Esnekliği**: Cache süresi ayarlanabilir + +## Sonuçlar + +### Olumlu +- En hızlı yanıt süresi (cache hit) +- Veri kalıcılığı +- Netflix üzerinde minimal yük + +### Olumsuz +- İki sistem senkronizasyonu +- Daha fazla kod karmaşıklığı + +### Yapılandırma +```env +REDIS_TTL_SECONDS=604800 # 7 gün +``` + +## Tarih + +2025-02-27 diff --git a/doc/decisions/ADR-003-api-key-strategy.md b/doc/decisions/ADR-003-api-key-strategy.md new file mode 100644 index 0000000..8655173 --- /dev/null +++ b/doc/decisions/ADR-003-api-key-strategy.md @@ -0,0 +1,63 @@ +# ADR-003: Named API Keys Stratejisi + +## Durum + +Kabul edildi + +## Bağlam + +API güvenliği için authentication yöntemi belirlememiz gerekiyor. Birden fazla frontend olacak (Web, Mobile, Admin). + +## Seçenekler + +1. **Tek API Key**: Tüm frontend'ler aynı key'i kullanır +2. **Named API Keys**: Her frontend için ayrı key +3. **Database API Keys**: Key'ler DB'de tutulur, yönetim paneli ile + +## Karar + +**Named API Keys** (.env'de tanımlı) seçildi. + +## Yapılandırma + +```env +API_KEY_WEB=web-frontend-key-xxx +API_KEY_MOBILE=mobile-app-key-yyy +API_KEY_ADMIN=admin-key-zzz +``` + +## Gerekçe + +### Tek Key Eksikleri +- Hangi frontend'in istek attığı belli değil +- Tek bir frontend engellenemez +- Audit trail yok + +### Database Key Eksikleri +- Ekstra kompleksite +- Yönetim paneli gerektirir +- Başlangıç için overkill + +### Named Keys Avantajları +1. **İzlenebilirlik**: Hangi frontend'in istek attığı bilinir +2. **Kontrol**: Tek bir frontend'in erişimi kapatılabilir +3. **Basitlik**: .env ile yönetim +4. **Güvenlik**: Her frontend için ayrı secret + +## Sonuçlar + +### Olumlu +- Frontend bazlı rate limiting +- Kolay key rotasyonu +- Basit yapılandırma + +### Olumsuz +- Yeni frontend için .env güncellemesi gerekir +- Key yönetimi manuel + +### Gelecek +Gerekirse Database API Keys sistemine geçiş yapılabilir. + +## Tarih + +2025-02-27 diff --git a/doc/ops.md b/doc/ops.md new file mode 100644 index 0000000..44d4e0c --- /dev/null +++ b/doc/ops.md @@ -0,0 +1,291 @@ +# Netflix Scraper API - Operasyon Dokümantasyonu + +## Hızlı Başlangıç + +### Gereksinimler + +- Docker & Docker Compose +- Node.js 20+ (local development için) + +### Tek Komut ile Başlatma (Development) + +```bash +# .env dosyasını oluştur +cp .env.example .env + +# .env dosyasını düzenle +nano .env + +# Başlat +docker compose -f docker-compose.dev.yml up --build +``` + +**Hepsi bu kadar!** Uygulama şu adreste çalışacak: `http://localhost:3000` + +--- + +## Ortamlar + +### Development + +```bash +docker compose -f docker-compose.dev.yml up --build +``` + +Özellikler: +- Hot reload aktif +- Tüm loglar görünür +- Debug modu + +### Production + +```bash +docker compose up --build -d +``` + +Özellikler: +- Multi-stage build +- Non-root user +- Minimal image +- Production optimizations + +--- + +## Environment Değişkenleri + +### .env Dosyası + +```env +# === Server === +PORT=3000 +NODE_ENV=development + +# === PostgreSQL === +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 +POSTGRES_USER=postgres +POSTGRES_PASSWORD=your-secure-password +POSTGRES_DB=netflix_scraper + +# === Redis === +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_TTL_SECONDS=604800 + +# === Rate Limiting === +RATE_LIMIT_WINDOW_MS=60000 +RATE_LIMIT_MAX_REQUESTS=30 + +# === API Keys === +API_KEY_WEB=web-key-change-me +API_KEY_MOBILE=mobile-key-change-me +API_KEY_ADMIN=admin-key-secret +``` + +### Değişken Açıklamaları + +| Değişken | Açıklama | Varsayılan | +|----------|----------|------------| +| `PORT` | Sunucu portu | 3000 | +| `NODE_ENV` | Ortam (development/production) | development | +| `REDIS_TTL_SECONDS` | Cache süresi (saniye) | 604800 (7 gün) | +| `RATE_LIMIT_WINDOW_MS` | Rate limit penceresi (ms) | 60000 (1 dk) | +| `RATE_LIMIT_MAX_REQUESTS` | Max istek sayısı | 30 | + +--- + +## Migration + +Migration'lar otomatik olarak container başlatılırken çalışır. Manuel çalıştırmak için: + +```bash +# Container içinde +docker compose exec app npx prisma migrate deploy + +# Local +npx prisma migrate deploy +``` + +### Yeni Migration Oluşturma + +```bash +npx prisma migrate dev --name description_of_change +``` + +--- + +## Loglama + +Tüm loglar JSON formatında structured logging ile yazılır. + +### Log Seviyeleri + +| Seviye | Açıklama | +|--------|----------| +| `debug` | Detaylı debug bilgisi | +| `info` | Genel bilgi | +| `warn` | Uyarı | +| `error` | Hata | + +### Log Formatı + +```json +{ + "timestamp": "2025-02-27T10:00:00.000Z", + "level": "info", + "message": "Server started", + "service": "netflix-scraper-api", + "port": 3000 +} +``` + +### Log Seviyesi Ayarlama + +```env +LOG_LEVEL=debug +``` + +--- + +## Health Check + +### Endpoints + +- `GET /health` - Basit sağlık kontrolü +- `GET /ready` - Bağımlılık kontrolü (DB + Redis) + +### Docker Health Check + +Container'lar otomatik health check ile izlenir: + +```yaml +healthcheck: + test: ["CMD", "wget", "--spider", "http://localhost:3000/health"] + interval: 30s + timeout: 10s + retries: 3 +``` + +--- + +## Troubleshooting + +### Database Bağlantı Hatası + +``` +Error: Can't reach database server +``` + +**Çözüm:** +1. PostgreSQL container'ın çalıştığını kontrol et +2. `POSTGRES_HOST` değerini kontrol et (docker'da `postgres` olmalı) +3. Health check'i bekle: `docker compose logs postgres` + +### Redis Bağlantı Hatası + +``` +Error: Redis connection failed +``` + +**Çözüm:** +1. Redis container'ın çalıştığını kontrol et +2. `REDIS_HOST` değerini kontrol et + +### Migration Hatası + +``` +Error: P3005: The database schema is not empty +``` + +**Çözüm:** +```bash +# Veritabanını sıfırla +docker compose down -v +docker compose up --build +``` + +### Port Kullanımda Hatası + +``` +Error: port is already allocated +``` + +**Çözüm:** +```bash +# Hangi process kullanıyor +lsof -i :3000 + +# Process'i durdur +kill -9 +``` + +--- + +## Yararlı Komutlar + +### Container Yönetimi + +```bash +# Container'ları durdur +docker compose down + +# Container'ları sil (volumes dahil) +docker compose down -v + +# Logları görüntüle +docker compose logs -f app + +# Container içine gir +docker compose exec app sh +``` + +### Database İşlemleri + +```bash +# Prisma studio aç +docker compose exec app npx prisma studio + +# Database schema görüntüle +docker compose exec app npx prisma db pull + +# Seed çalıştır +docker compose exec app npm run prisma:seed +``` + +### Redis İşlemleri + +```bash +# Redis CLI +docker compose exec redis redis-cli + +# Tüm cache'i temizle +docker compose exec redis redis-cli FLUSHALL +``` + +--- + +## Backup & Restore + +### PostgreSQL Backup + +```bash +docker compose exec postgres pg_dump -U postgres netflix_scraper > backup.sql +``` + +### PostgreSQL Restore + +```bash +cat backup.sql | docker compose exec -T postgres psql -U postgres netflix_scraper +``` + +--- + +## Monitoring (Opsiyonel) + +### Metrics Endpoint (Yakında) + +``` +GET /metrics +``` + +Prometheus uyumlu metrics döndürür. diff --git a/doc/overview.md b/doc/overview.md new file mode 100644 index 0000000..eb888b6 --- /dev/null +++ b/doc/overview.md @@ -0,0 +1,82 @@ +# Netflix Scraper API - Proje Özeti + +## Proje Hakkında + +Netflix Scraper API, Netflix içerik sayfalarından film/dizi bilgilerini çeken bir backend servisidir. URL gönderilerek içerik bilgileri çekilir, önbelleğe alınır ve geri döndürülür. + +## Neden Oluşturuldu? + +- **Otomatik İçerik Toplama**: Netflix URL'lerinden otomatik olarak içerik bilgisi çekmek +- **Performans**: Redis cache ile tekrarlayan isteklerde hızlı yanıt +- **Kalıcılık**: PostgreSQL ile verilerin kalıcı olarak saklanması +- **Gerçek Zamanlı**: Socket.IO ile canlı ilerleme bildirimleri + +## Teknoloji Yığını + +| Katman | Teknoloji | +|--------|-----------| +| Runtime | Node.js 20+ | +| Framework | Express.js | +| Database | PostgreSQL 16 | +| Cache | Redis 7 | +| Real-time | Socket.IO | +| Scraper | Cheerio | +| ORM | Prisma | + +## Mimari + +``` +┌─────────────────┐ +│ Frontend │ +│ (Web/Mobile) │ +└────────┬────────┘ + │ HTTP + API Key + ▼ +┌─────────────────┐ ┌─────────────┐ +│ Express API │────▶│ Redis │ +│ (Port 3000) │ │ (Cache) │ +└────────┬────────┘ └─────────────┘ + │ + ▼ +┌─────────────────┐ ┌─────────────┐ +│ Scraper Service │────▶│ PostgreSQL │ +│ (Cheerio) │ │ (Data) │ +└─────────────────┘ └─────────────┘ +``` + +## Veri Akışı + +1. **İstek Alınır**: `POST /api/getinfo` endpoint'ine URL gönderilir +2. **Cache Kontrolü**: Redis'te veri var mı diye bakılır +3. **DB Kontrolü**: PostgreSQL'de veri var mı diye bakılır +4. **Scraping**: Netflix'ten veri çekilir (cache/DB'de yoksa) +5. **Kaydetme**: Veri DB'ye ve cache'e yazılır +6. **Yanıt**: JSON formatında veri döndürülür + +## Önemli Kararlar + +### ADR-001: Cheerio Seçimi +Playwright yerine Cheerio seçildi. Başlangıç için yeterli, daha hafif ve hızlı. Gerekirse Playwright'a geçiş yapılabilir. + +### ADR-002: Hybrid Cache Stratejisi +Cache → DB → Netflix sıralaması ile veri alınır. Bu sayede: +- En hızlı yanıt cache'ten gelir +- Cache'te yoksa DB'den gelir +- İlk istek hariç Netflix'e istek atılmaz + +### ADR-003: Named API Keys +Her frontend için ayrı API key kullanılır. Bu sayede: +- Hangi frontend'in istek attığı takip edilebilir +- Gerekirse tek bir frontend'in erişimi kapatılabilir + +## Güncellemeler + +| Tarih | Güncelleme | +|-------|-----------| +| 2025-02-27 | Proje oluşturuldu | + +## İlgili Dokümanlar + +- [API Dokümantasyonu](./api.md) +- [Operasyon Notları](./ops.md) +- [Socket Events](./socket-events.md) diff --git a/doc/socket-events.md b/doc/socket-events.md new file mode 100644 index 0000000..adc4e0a --- /dev/null +++ b/doc/socket-events.md @@ -0,0 +1,256 @@ +# Netflix Scraper API - Socket.IO Events + +## Bağlantı + +### URL +``` +ws://localhost:3000 +``` + +### Transport +- WebSocket +- Polling (fallback) + +--- + +## Client → Server Events + +### `job:subscribe` + +Bir job'ın güncellemelerine abone ol. + +**Payload:** +```typescript +jobId: string +``` + +**Örnek:** +```javascript +socket.emit('job:subscribe', '550e8400-e29b-41d4-a716-446655440000'); +``` + +--- + +### `job:unsubscribe` + +Job aboneliğini iptal et. + +**Payload:** +```typescript +jobId: string +``` + +**Örnek:** +```javascript +socket.emit('job:unsubscribe', '550e8400-e29b-41d4-a716-446655440000'); +``` + +--- + +## Server → Client Events + +### `job:progress` + +Job ilerleme durumu güncellendiğinde gönderilir. + +**Payload:** +```typescript +{ + jobId: string; + progress: number; // 0-100 arası + status: string; // "pending" | "processing" | "completed" | "failed" + step: string; // Mevcut adım açıklaması +} +``` + +**Örnek:** +```json +{ + "jobId": "550e8400-e29b-41d4-a716-446655440000", + "progress": 50, + "status": "processing", + "step": "Scraping Netflix" +} +``` + +**Adımlar:** +| Step | Progress | Açıklama | +|------|----------|----------| +| `created` | 0 | Job oluşturuldu | +| `checking_cache` | 10 | Cache kontrol ediliyor | +| `checking_database` | 30 | Database kontrol ediliyor | +| `scraping_netflix` | 50 | Netflix'ten veri çekiliyor | +| `saving_to_database` | 80 | Veritabanına kaydediliyor | +| `completed` | 100 | İşlem tamamlandı | + +--- + +### `job:completed` + +Job başarıyla tamamlandığında gönderilir. + +**Payload:** +```typescript +{ + jobId: string; + data: GetInfoResponse; + source: "cache" | "database" | "netflix"; +} +``` + +**Örnek:** +```json +{ + "jobId": "550e8400-e29b-41d4-a716-446655440000", + "data": { + "title": "Hayata Röveşata Çeken Adam", + "year": 2022, + "plot": "...", + "genres": ["18+", "Komedi"], + "cast": ["Tom Hanks", "Mariana Treviño", "Rachel Keller"], + "backdrop": "https://..." + }, + "source": "netflix" +} +``` + +--- + +### `job:error` + +Job sırasında hata oluştuğunda gönderilir. + +**Payload:** +```typescript +{ + jobId: string; + error: { + code: string; + message: string; + } +} +``` + +**Örnek:** +```json +{ + "jobId": "550e8400-e29b-41d4-a716-446655440000", + "error": { + "code": "SCRAPE_ERROR", + "message": "Failed to fetch Netflix page: 403" + } +} +``` + +--- + +## Kullanım Örneği + +### JavaScript (Browser) + +```javascript +import { io } from 'socket.io-client'; + +// Bağlan +const socket = io('http://localhost:3000'); + +// Bağlantı başarılı +socket.on('connect', () => { + console.log('Connected:', socket.id); +}); + +// Job oluştur (API üzerinden) +const response = await fetch('http://localhost:3000/api/getinfo/async', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': 'your-api-key' + }, + body: JSON.stringify({ + url: 'https://www.netflix.com/tr/title/81616256' + }) +}); + +const { data: { jobId } } = await response.json(); + +// Job'a abone ol +socket.emit('job:subscribe', jobId); + +// İlerleme dinle +socket.on('job:progress', (data) => { + console.log(`Progress: ${data.progress}% - ${data.step}`); +}); + +// Tamamlanma dinle +socket.on('job:completed', (data) => { + console.log('Completed:', data.data); + socket.emit('job:unsubscribe', jobId); +}); + +// Hata dinle +socket.on('job:error', (data) => { + console.error('Error:', data.error); +}); +``` + +### React Hook + +```typescript +import { useEffect, useState } from 'react'; +import { io, Socket } from 'socket.io-client'; + +interface JobProgress { + jobId: string; + progress: number; + status: string; + step: string; +} + +export function useJobProgress(jobId: string | null) { + const [progress, setProgress] = useState(null); + const [data, setData] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + if (!jobId) return; + + const socket = io('http://localhost:3000'); + + socket.emit('job:subscribe', jobId); + + socket.on('job:progress', setProgress); + socket.on('job:completed', (result) => { + setData(result.data); + socket.emit('job:unsubscribe', jobId); + }); + socket.on('job:error', (err) => { + setError(err.error); + socket.emit('job:unsubscribe', jobId); + }); + + return () => { + socket.emit('job:unsubscribe', jobId); + socket.disconnect(); + }; + }, [jobId]); + + return { progress, data, error }; +} +``` + +--- + +## CORS Yapılandırması + +Production'da CORS ayarlarını yapılandırın: + +```typescript +// src/config/socket.ts +io = new Server(httpServer, { + cors: { + origin: ['https://yourdomain.com', 'https://admin.yourdomain.com'], + methods: ['GET', 'POST'], + credentials: true, + }, +}); +``` diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..cb0c372 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,95 @@ +# =========================================== +# Development Docker Compose +# Hot reload enabled, single command startup +# =========================================== +services: + app: + build: + context: . + dockerfile: Dockerfile.dev + container_name: netflix-scraper-api-dev + restart: unless-stopped + ports: + - "${PORT:-3000}:3000" + environment: + - NODE_ENV=development + - PORT=3000 + - POSTGRES_HOST=postgres + - POSTGRES_PORT=5432 + - POSTGRES_USER=${POSTGRES_USER:-postgres} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres} + - POSTGRES_DB=${POSTGRES_DB:-netflix_scraper} + - REDIS_HOST=redis + - REDIS_PORT=6379 + - REDIS_TTL_SECONDS=${REDIS_TTL_SECONDS:-604800} + - RATE_LIMIT_WINDOW_MS=${RATE_LIMIT_WINDOW_MS:-60000} + - RATE_LIMIT_MAX_REQUESTS=${RATE_LIMIT_MAX_REQUESTS:-30} + - API_KEY_WEB=${API_KEY_WEB:-web-dev-key-change-me} + - API_KEY_MOBILE=${API_KEY_MOBILE:-mobile-dev-key-change-me} + - API_KEY_ADMIN=${API_KEY_ADMIN:-admin-dev-key-secret} + - TMDB_API_KEY=${TMDB_API_KEY:-your-tmdb-api-key-here} + - TMDB_ACCESS_TOKEN=${TMDB_ACCESS_TOKEN:-your-tmdb-access-token-here} + volumes: + - ./src:/app/src:delegated + - ./prisma:/app/prisma:delegated + - ./package.json:/app/package.json:delegated + - node_modules_data:/app/node_modules + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + networks: + - netflix-scraper-network + + postgres: + image: postgres:16-alpine + container_name: netflix-scraper-postgres-dev + restart: unless-stopped + ports: + - "5432:5432" + environment: + - POSTGRES_USER=${POSTGRES_USER:-postgres} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres} + - POSTGRES_DB=${POSTGRES_DB:-netflix_scraper} + volumes: + - postgres_data_dev:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-netflix_scraper}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + networks: + - netflix-scraper-network + + redis: + image: redis:7-alpine + container_name: netflix-scraper-redis-dev + restart: unless-stopped + command: redis-server --appendonly yes + volumes: + - redis_data_dev:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 5s + networks: + - netflix-scraper-network + +volumes: + postgres_data_dev: + redis_data_dev: + node_modules_data: + +networks: + netflix-scraper-network: + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..de86eab --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,94 @@ +# =========================================== +# Production Docker Compose +# Optimized for production deployment +# =========================================== +services: + app: + build: + context: . + dockerfile: Dockerfile + container_name: netflix-scraper-api + restart: unless-stopped + ports: + - "${PORT:-3000}:3000" + environment: + - NODE_ENV=production + - PORT=3000 + - POSTGRES_HOST=postgres + - POSTGRES_PORT=5432 + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=${POSTGRES_DB} + - REDIS_HOST=redis + - REDIS_PORT=6379 + - REDIS_TTL_SECONDS=${REDIS_TTL_SECONDS:-604800} + - RATE_LIMIT_WINDOW_MS=${RATE_LIMIT_WINDOW_MS:-60000} + - RATE_LIMIT_MAX_REQUESTS=${RATE_LIMIT_MAX_REQUESTS:-30} + - API_KEY_WEB=${API_KEY_WEB} + - API_KEY_MOBILE=${API_KEY_MOBILE} + - API_KEY_ADMIN=${API_KEY_ADMIN} + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + networks: + - netflix-scraper-network + security_opt: + - no-new-privileges:true + read_only: true + tmpfs: + - /tmp + + postgres: + image: postgres:16-alpine + container_name: netflix-scraper-postgres + restart: unless-stopped + environment: + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=${POSTGRES_DB} + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + networks: + - netflix-scraper-network + security_opt: + - no-new-privileges:true + + redis: + image: redis:7-alpine + container_name: netflix-scraper-redis + restart: unless-stopped + command: redis-server --appendonly yes + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 5s + networks: + - netflix-scraper-network + security_opt: + - no-new-privileges:true + +volumes: + postgres_data: + redis_data: + +networks: + netflix-scraper-network: + driver: bridge diff --git a/docs/brainstorms/2026-02-27-db-content-list-brainstorm.md b/docs/brainstorms/2026-02-27-db-content-list-brainstorm.md new file mode 100644 index 0000000..07cd9ae --- /dev/null +++ b/docs/brainstorms/2026-02-27-db-content-list-brainstorm.md @@ -0,0 +1,29 @@ +--- +date: 2026-02-27 +topic: db-content-list +--- + +# DB Content List (Web) + +## What We're Building +Veritabanında kayıtlı film ve dizileri web arayüzünde kart görünümü ile listeleyeceğiz. Her kartta poster/backdrop görseli, başlık, tür, yıl ve temel içerik bilgileri gösterilecek. + +İlk sürümde veri kaynağı yalnızca mevcut DB olacak. Harici API ile poster tamamlama veya enrichment yapılmayacak. + +## Why This Approach +Seçilen yaklaşım: server API endpoint + frontend Mantine card grid. + +Bu yaklaşım, hızlı teslim ve temiz ayrım sağlar: backend yalnızca kayıtlı içerikleri döner, frontend görselleştirmeyi üstlenir. Sonradan filtre, pagination ve sıralama gibi eklemeler düşük maliyetle yapılabilir. + +## Key Decisions +- Web arayüzü kullanılacak: Kullanıcı hedefi doğrudan UI üzerinden liste görmek. +- Kart görünümü seçildi: Poster + temel metadata ile hızlı taranabilir deneyim. +- Veri kaynağı sadece DB: İlk sürümde kapsam kontrolü ve hızlı teslim için. +- Mantine bileşenleri kullanılacak: Mevcut frontend stack ile uyum için. + +## Open Questions +- Pagination ilk sürümde gerekli mi, yoksa 100 kayıt sınırı yeterli mi? +- Kartta hangi alanlar zorunlu tutulmalı (oy puanı, cast, genre sayısı)? + +## Next Steps +-> `/workflows:plan` diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..d2e7761 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,23 @@ +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' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..072a57e --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + frontend + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..4e351d5 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3865 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "license": "ISC", + "dependencies": { + "@fortawesome/fontawesome-svg-core": "^7.2.0", + "@fortawesome/free-brands-svg-icons": "^7.2.0", + "@fortawesome/free-solid-svg-icons": "^7.2.0", + "@fortawesome/react-fontawesome": "^3.2.0", + "@mantine/core": "^8.3.15", + "@mantine/hooks": "^8.3.15", + "@tabler/icons-react": "^3.37.1", + "postcss": "^8.5.6", + "postcss-preset-mantine": "^1.18.0", + "postcss-simple-vars": "^7.0.1", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.0", + "vite": "^7.3.1" + } + }, + "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/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.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.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", + "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "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.3", + "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.3", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", + "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", + "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/@floating-ui/core": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", + "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.4", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.27.18", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.18.tgz", + "integrity": "sha512-xJWJxvmy3a05j643gQt+pRbht5XnTlGpsEsAPnMi5F5YTOEEJymA90uZKBD8OvIv5XvZ1qi4GcccSlqT3Bq44Q==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.7", + "@floating-ui/utils": "^0.2.10", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.7.tgz", + "integrity": "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.5" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.2.0.tgz", + "integrity": "sha512-IpR0bER9FY25p+e7BmFH25MZKEwFHTfRAfhOyJubgiDnoJNsSvJ7nigLraHtp4VOG/cy8D7uiV0dLkHOne5Fhw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.2.0.tgz", + "integrity": "sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q==", + "license": "MIT", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-brands-svg-icons": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-7.2.0.tgz", + "integrity": "sha512-VNG8xqOip1JuJcC3zsVsKRQ60oXG9+oYNDCosjoU/H9pgYmLTEwWw8pE0jhPz/JWdHeUuK6+NQ3qsM4gIbdbYQ==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.2.0.tgz", + "integrity": "sha512-YTVITFGN0/24PxzXrwqCgnyd7njDuzp5ZvaCx5nq/jg55kUYd94Nj8UTchBdBofi/L0nwRfjGOg0E41d2u9T1w==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/react-fontawesome": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-3.2.0.tgz", + "integrity": "sha512-E9Gu1hqd6JussVO26EC4WqRZssXMnQr2ol7ZNWkkFOH8jZUaxDJ9Z9WF9wIVkC+kJGXUdY3tlffpDwEKfgQrQw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~6 || ~7", + "react": "^18.0.0 || ^19.0.0" + } + }, + "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/@mantine/core": { + "version": "8.3.15", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.15.tgz", + "integrity": "sha512-wBn/GogB4x7a2Uj7Ztt3amRaApjED+9XqfE4wyCLh88R7KV55k9vnTdCx+irI/GLOOu9tXNUGm3a4t5sTajwkQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.27.16", + "clsx": "^2.1.1", + "react-number-format": "^5.4.4", + "react-remove-scroll": "^2.7.1", + "react-textarea-autosize": "8.5.9", + "type-fest": "^4.41.0" + }, + "peerDependencies": { + "@mantine/hooks": "8.3.15", + "react": "^18.x || ^19.x", + "react-dom": "^18.x || ^19.x" + } + }, + "node_modules/@mantine/hooks": { + "version": "8.3.15", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.15.tgz", + "integrity": "sha512-AUSnpUlzttHzJht3CJ1YWi16iy6NWRwtyWO5RLGHHsmiW05DyG0qOPKF8+R5dLHuOCnl3XOu4roI2Y1ku9U04Q==", + "license": "MIT", + "peerDependencies": { + "react": "^18.x || ^19.x" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tabler/icons": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.37.1.tgz", + "integrity": "sha512-neLCWkuyNHEPXCyYu6nbN4S3g/59BTa4qyITAugYVpq1YzYNDOZooW7/vRWH98ZItXAudxdKU8muFT7y1PqzuA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + } + }, + "node_modules/@tabler/icons-react": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.37.1.tgz", + "integrity": "sha512-R7UE71Jji7i4Su56Y9zU1uYEBakUejuDJvyuYVmBuUoqp/x3Pn4cv2huarexR3P0GJ2eHg4rUj9l5zccqS6K/Q==", + "license": "MIT", + "dependencies": { + "@tabler/icons": "" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + }, + "peerDependencies": { + "react": ">= 16" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.15.tgz", + "integrity": "sha512-BgjLoRuSr0MTI5wA6gMw9Xy0sFudAaUuvrnjgGx9wZ522fYYLA5SYJ+1Y30vTcJEG+DRCyDHx/gzQVfofYzSdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "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/@typescript-eslint/eslint-plugin": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "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.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.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.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "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 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", + "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.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" + }, + "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.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", + "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.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "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 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", + "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.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "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/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "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.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" + }, + "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 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.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/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", + "integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "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-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/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.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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/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/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001774", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", + "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", + "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/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/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/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/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "dev": true, + "license": "ISC" + }, + "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-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.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", + "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", + "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.3", + "@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": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "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/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==", + "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/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/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/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-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "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/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-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/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-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/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "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==", + "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/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-mixins": { + "version": "12.1.2", + "resolved": "https://registry.npmjs.org/postcss-mixins/-/postcss-mixins-12.1.2.tgz", + "integrity": "sha512-90pSxmZVfbX9e5xCv7tI5RV1mnjdf16y89CJKbf/hD7GyOz1FCxcYMl8ZYA8Hc56dbApTKKmU9HfvgfWdCxlwg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-js": "^4.0.1", + "postcss-simple-vars": "^7.0.1", + "sugarss": "^5.0.0", + "tinyglobby": "^0.2.14" + }, + "engines": { + "node": "^20.0 || ^22.0 || >=24.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nested": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-7.0.2.tgz", + "integrity": "sha512-5osppouFc0VR9/VYzYxO03VaDa3e8F23Kfd6/9qcZTUI8P58GIYlArOET2Wq0ywSl2o2PjELhYOFI4W7l5QHKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-preset-mantine": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/postcss-preset-mantine/-/postcss-preset-mantine-1.18.0.tgz", + "integrity": "sha512-sP6/s1oC7cOtBdl4mw/IRKmKvYTuzpRrH/vT6v9enMU/EQEQ31eQnHcWtFghOXLH87AAthjL/Q75rLmin1oZoA==", + "license": "MIT", + "dependencies": { + "postcss-mixins": "^12.0.0", + "postcss-nested": "^7.0.2" + }, + "peerDependencies": { + "postcss": ">=8.0.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-simple-vars": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-simple-vars/-/postcss-simple-vars-7.0.1.tgz", + "integrity": "sha512-5GLLXaS8qmzHMOjVxqkk1TZPf1jMqesiI7qLhnlyERalG0sMbHIbJqrcnrpmZdKCLglHnRHoEBB61RtGTsj++A==", + "license": "MIT", + "engines": { + "node": ">=14.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.1" + } + }, + "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/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/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-number-format": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.4.tgz", + "integrity": "sha512-wOmoNZoOpvMminhifQYiYSTCLUDOiUbBunrMrMjA+dV52sY+vck1S4UhR6PkgnoCquvvMSeJjErXZ4qSaWCliA==", + "license": "MIT", + "peerDependencies": { + "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-textarea-autosize": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz", + "integrity": "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "use-composed-ref": "^1.3.0", + "use-latest": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/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/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/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/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/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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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/sugarss": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-5.0.1.tgz", + "integrity": "sha512-ctS5RYCBVvPoZAnzIaX5QSShK8ZiZxD5HUqSxlusvEMC+QZQIPCPOIJg6aceFX+K2rf4+SH89eu++h1Zmsr2nw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, + "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/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "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/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==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz", + "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.56.1", + "@typescript-eslint/parser": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1" + }, + "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 || ^10.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==", + "dev": true, + "license": "MIT" + }, + "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/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-composed-ref": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.4.0.tgz", + "integrity": "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", + "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-latest": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.3.0.tgz", + "integrity": "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==", + "license": "MIT", + "dependencies": { + "use-isomorphic-layout-effect": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/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/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..615d348 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,45 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@fortawesome/fontawesome-svg-core": "^7.2.0", + "@fortawesome/free-brands-svg-icons": "^7.2.0", + "@fortawesome/free-solid-svg-icons": "^7.2.0", + "@fortawesome/react-fontawesome": "^3.2.0", + "@mantine/core": "^8.3.15", + "@mantine/hooks": "^8.3.15", + "@tabler/icons-react": "^3.37.1", + "postcss": "^8.5.6", + "postcss-preset-mantine": "^1.18.0", + "postcss-simple-vars": "^7.0.1", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.0", + "vite": "^7.3.1" + }, + "description": "This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.", + "main": "eslint.config.js", + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/frontend/postcss.config.cjs b/frontend/postcss.config.cjs new file mode 100644 index 0000000..bfba0dd --- /dev/null +++ b/frontend/postcss.config.cjs @@ -0,0 +1,14 @@ +module.exports = { + plugins: { + 'postcss-preset-mantine': {}, + 'postcss-simple-vars': { + variables: { + 'mantine-breakpoint-xs': '36em', + 'mantine-breakpoint-sm': '48em', + 'mantine-breakpoint-md': '62em', + 'mantine-breakpoint-lg': '75em', + 'mantine-breakpoint-xl': '88em', + }, + }, + }, +}; diff --git a/frontend/public/netflix.png b/frontend/public/netflix.png new file mode 100644 index 0000000000000000000000000000000000000000..08465ad7bb0898f91af855c77e98fb7a3cdb8087 GIT binary patch literal 5668 zcmZWt3s_ZE7GCF`>k;IDOjqq*qC_dg0zGh1DM^lsujEyhNj^9tS0DPcaA*P_3IAy;_{Tl`$JaSNEgvAD*7z$NpR+R)jkft? zrp%u-XZHMsGv_&A;lhQ3a`JNXXU%jv2F;n5wfX0`Ab{R~n~*T_#e%A9%_+McPb)lj zVed;x|9VxuvCJ_gW>kr-kWDd(yT1}ESe)kT^VzzMOES|Af9f3hts^z*&)>F}4eM|3 zf9`tK^}@+(U)UOP;LW`1^Jfn?bX>k1zpv=#(dqYQwf*{I$AI`d*RCH-E-Km8asPPy z!IYvEr#gP^S=74bv_4~dJnXsKn0{`1{HQs%>%XXpH|axUcBuXCwS7m6Ox+HAasS)) zl%jhF+Kxk09ax+9-LAj!)pxglSo8Invp2TQ>1g?NaARLldrH(yMYpop&fBZvMfg4n z!dh`w{qT(twXpt1eSG`H0rAfj1+Yk&`8E&k+mQ|#2N&M{ea_+y9f2^_3T3T9Ay$!lVQK|_T`e>`Fsee^J3>P-AyoTGYG@_QtpwF3srEeKFmBgh zX0v1df3zY-_!z(Tkl6zx17NJ^9=MxUC^$-Du}Kh82iYeHUtlK0?4T5yZLm6r(mGO7 z^LdmArHX*A&48F1N??K&3jEh%s?1i%HvI>)S7V1M6-Ogg#O{n1nInzorOXBq^~eq; zz3L=*Ln(vaL<#!l5GPsDaA!PaD|WS@XYH4K8qJWL0j)NI=j zI3B&l3UL!GArmbSTPf&eJp64oG9m-9<%0eR_Zg50tr6U(BEiF-koPi=+ z#eFWd!hVEg_Iz9^zfI~4yyguj9#;)WfQ;XT5qPL<$!0@Y9Hyr(Am&*cV7JQ5 zF{vq%(G~Dd!&*W0rFhlh5LWT!M7%U6J6`k-D z2p>m~gK%_$Xihv0?#D^juC0PPfsiWCiVW)n|7dx*F#*(@(akr8?$}BjlwTTRw8F%C zaK{kIu5v*wq?*xCpe@vO^m1I%NLUBjKpsh=zBCIIGv&{EK}k^R7_ z?q`KWqzkjMLIdSd4i~#vIZ@*tfg5t|yeYvXPR&L1*g_u5F~UenWcW$Ycsp}Pi0lKR zMoq&doSKhu2^ZrK8zf>_*ehR+2G>?9v|gFe@D}AUwNz032x4l9pguxeyL*XjC?)T~ z(PWM=Kx7Y$u#>qlyahaq56SRvUMwaVUg3xJTnQpMz$!tVNMa1kf`$spVWtf}<&6T- zzfP2@SS-1`SP)b$!3)fQhOJW#Zi^Mj#(*$^{c$fIu8#7!4kx|sjx3A z;1wwSu}cI<;o=U+0^P!WYB;l&d zgFAiB1RZB$YP}%AN4$S6z>RuP_B_Kyn9TddSsQrPQzEa{1KU8}wRBwbKDEE$(V&TR zsnCW=@EQsjd*bjY&_YSy9zPL=enZ7P-3lIl&|W?vM)*JRlH7I|+zMMmsv`f4=t zDs);z*wyxWPw!7{m$R>aI4r$+d%P$kj;YP(+sEAtoqwygXz`W~A0EzbBYc` z)TcLAZ~O527ljA*itHyVIG6VAxLnl!%i^@fWgWrO4=nzBMEb#BPFttd4(^`U_G;R1 zwva3LJ7jnn^E5WS5V<}SvFN_c*Ts9#1;D~ZrWNT?mrBhc%}LVxmfr&NFe z-w@F2cb?EsaG;_H$vX&W-#fN0#)o2r_x4L5LVud40}D=K;d@|y7N~d+#CtQUGmt`5 z7beG>DVPS##YjRp|I(GwYiZgR8{kdV2zoj$psm!GC3q3#+R}%W$`DS`bsbd|gyT6q z(ZcfZYv(9<;U6OzcH#}LE*GAO0(T&VW{mL4V8ELRY7|HCztM&A(VeI?P&-H7;x4wA zD1i!A-aC{aZ2?`rK$$mHSyKfJj#ZEvz(Jrn@TJgcFVU zb_=fM0xad9`$$lN7v;K0uq&Xcd>iCR;3&iaR*8DAenkc=USp85ZTX94^6Od&Dr6n$ zTmKS2^9V!jpv}ir|_UO5f8 z7}r=%jR`-$PoI6z&jzZOzarch$j^(|J>RDdcb@uT*-A=4Dr(U}A-JQ*S;DyR9EcHv zL5zPzYUc694p6&O)H{(RiM#1CTDdHY+pu~3F`C=OmQ0o6en^BMBE}efPBE*!C5WM* zx{i|j#ZV+_H~iW;Dh>H#B*A>V+@f}J@GJ4BVq4fk#T;@1*fGK~J{sKoIlR6qE}PTS zON2^_H{|HwmR{o1q5zwevRfFh&>#u$2=|IqDOFzqR5-pik8d=4Uz5NS!H?Fg{C~4G zv^cg*fJHp#n}UaQz8z2DPbt-n=HeSc>=P^KS$yUy0pfVM*wKj3Wh{-VLjaFaLcDUa zwH0ryD4f#S04Y>r-t5wcyBJS%pP@F;KjA*t3owoQ+=f2Kkk8D z^X>Rg+K=R{OcAhEIk2tX-b-myy>Mum*iG63G@S$JrG7|Z+4=_p{MDPOI@$4y;~ z-}4qU370!*c^oo)>Rs~rHX8A9h*F9}(bZ6s@%F0`VpkL5YUHhv&hplQ4nJ28*Z>oT z=QTTs>+pA~i5B-@Tu2$cQoknvuThyfrZ&XP&xmL5)-8!OcxPK>a7#qYJxmCmuy}45 zqi2ZgHq4xZcI`5{xLM-Q!z)P3AEPsJImxu)Z2|sa5HLjTDM2dA5P=z^GQxKkk>c|Z z6L`}OY&~sv>GF5o@KK0QCf8;G%BjI9!_p&D>B4S28Qnt(;U!ebc8cU=J;0Axp7Z>a z0V0IcdqYdcUGTk85vEry+GacoF%*0#!f48$Yt3$pmF|}MBCMofO5sV&{R;YZL%~Y2 zV!Oc{79vZjeDzFRO~P@#J?BqW3dVRIcD@<&TYH;o`RAnql;E#Ub-C=9_D5~`H3c`$ TO}>D?@WO;K$qBngX>a`(ABQ}8 literal 0 HcmV?d00001 diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..3d503dc --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,2 @@ +/* App styles - Mantine handles most styling */ + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..ad750c2 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,8 @@ +import { MoviesPage } from './pages/MoviesPage' +import './App.css' + +function App() { + return +} + +export default App diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..447e557 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,10 @@ +body { + margin: 0; + padding: 0; + background-color: #1a1a1b; + min-height: 100vh; +} + +#root { + min-height: 100vh; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..0cfb5e4 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,19 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { MantineProvider, createTheme } from '@mantine/core' +import '@mantine/core/styles.css' +import './index.css' +import App from './App.tsx' + +const theme = createTheme({ + primaryColor: 'red', + fontFamily: 'Inter, system-ui, Avenir, Helvetica, Arial, sans-serif', +}) + +createRoot(document.getElementById('root')!).render( + + + + + , +) diff --git a/frontend/src/pages/MoviesPage.tsx b/frontend/src/pages/MoviesPage.tsx new file mode 100644 index 0000000..5563fc3 --- /dev/null +++ b/frontend/src/pages/MoviesPage.tsx @@ -0,0 +1,208 @@ +import { useEffect, useMemo, useState } from 'react' +import { + Alert, + Badge, + Card, + Container, + Grid, + Group, + Image, + Loader, + Paper, + SegmentedControl, + Stack, + Text, + Title, +} from '@mantine/core' +import { IconAlertCircle, IconDeviceTv, IconMovie } from '@tabler/icons-react' + +type ContentType = 'movie' | 'tvshow' + +interface ContentListItem { + title: string + year: number | null + plot: string | null + ageRating: string | null + type: ContentType + genres: string[] + backdrop: string | null + currentSeason: number | null +} + +interface ContentListResponse { + success: boolean + data?: ContentListItem[] + error?: { + message: string + } +} + +export function MoviesPage() { + const [items, setItems] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [typeFilter, setTypeFilter] = useState<'all' | ContentType>('all') + + useEffect(() => { + const controller = new AbortController() + + const loadContent = async () => { + setLoading(true) + setError(null) + try { + const params = new URLSearchParams() + params.append('limit', '100') + if (typeFilter !== 'all') { + params.append('type', typeFilter) + } + + const response = await fetch(`/api/content?${params.toString()}`, { + method: 'GET', + headers: { + 'X-API-Key': 'web-dev-key-change-me', + }, + signal: controller.signal, + }) + + const data: ContentListResponse = await response.json() + if (data.success && data.data) { + setItems(data.data) + return + } + + setError(data.error?.message || 'Liste alınamadı') + } catch { + if (!controller.signal.aborted) { + setError('Bağlantı hatası') + } + } finally { + if (!controller.signal.aborted) { + setLoading(false) + } + } + } + + void loadContent() + + return () => controller.abort() + }, [typeFilter]) + + const pageTitle = useMemo(() => { + if (typeFilter === 'movie') return 'Film Listesi' + if (typeFilter === 'tvshow') return 'Dizi Listesi' + return 'Film ve Dizi Listesi' + }, [typeFilter]) + + return ( + + + + +
+ {pageTitle} + + Veriler doğrudan veritabanından okunur. + +
+ setTypeFilter(value as 'all' | ContentType)} + data={[ + { label: 'Tümü', value: 'all' }, + { label: 'Filmler', value: 'movie' }, + { label: 'Diziler', value: 'tvshow' }, + ]} + /> +
+
+ + {error && ( + } color="red" title="Hata"> + {error} + + )} + + {loading ? ( + + + + ) : ( + + {items.map((item) => ( + + + + {item.backdrop ? ( + {item.title} + ) : ( + + {item.type === 'movie' ? ( + + ) : ( + + )} + + )} + + + + + + {item.title} + + + {item.type === 'movie' ? 'Film' : 'Dizi'} + + + + + {item.year && {item.year}} + {item.ageRating && {item.ageRating}} + {item.currentSeason && item.type === 'tvshow' && ( + + S{item.currentSeason} + + )} + + + + {item.plot || 'Açıklama bulunamadı.'} + + + + {item.genres.slice(0, 3).map((genre) => ( + + {genre} + + ))} + + + + Netflix + + + ))} + + )} + + {!loading && !error && items.length === 0 && ( + + Kayıt bulunamadı. + + )} +
+
+ ) +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..a9b5a59 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..be25542 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true, + }, + }, + }, + css: { + modules: { + localsConvention: 'camelCase', + }, + }, +}) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c546e31 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2301 @@ +{ + "name": "netflix-scraper-api", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "netflix-scraper-api", + "version": "1.0.0", + "dependencies": { + "@prisma/client": "^5.22.0", + "cheerio": "^1.0.0", + "express": "^4.21.1", + "express-rate-limit": "^7.4.1", + "ioredis": "^5.4.1", + "socket.io": "^4.8.1", + "uuid": "^10.0.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^22.9.0", + "@types/uuid": "^10.0.0", + "prisma": "^5.22.0", + "tsx": "^4.19.2", + "typescript": "^5.6.3" + }, + "engines": { + "node": ">=20.0.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/@ioredis/commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", + "license": "MIT" + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "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/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz", + "integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.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/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "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/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/engine.io": { + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz", + "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/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/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/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/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/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ioredis": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.0.tgz", + "integrity": "sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ioredis/node_modules/debug": { + "version": "4.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/ioredis/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/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/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "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/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "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/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.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/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.18.3" + } + }, + "node_modules/socket.io-adapter/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/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/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/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io/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/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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/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-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", + "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "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/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/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" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..66bcb83 --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "netflix-scraper-api", + "version": "1.0.0", + "description": "Netflix content scraper API with caching and real-time updates", + "main": "dist/index.js", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate deploy", + "prisma:seed": "tsx prisma/seed.ts", + "lint": "eslint src --ext .ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@prisma/client": "^5.22.0", + "cheerio": "^1.0.0", + "express": "^4.21.1", + "express-rate-limit": "^7.4.1", + "ioredis": "^5.4.1", + "socket.io": "^4.8.1", + "uuid": "^10.0.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^22.9.0", + "@types/uuid": "^10.0.0", + "prisma": "^5.22.0", + "tsx": "^4.19.2", + "typescript": "^5.6.3" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/prisma/migrations/20250227000000_init/migration.sql b/prisma/migrations/20250227000000_init/migration.sql new file mode 100644 index 0000000..a0b109d --- /dev/null +++ b/prisma/migrations/20250227000000_init/migration.sql @@ -0,0 +1,95 @@ +-- CreateEnum +CREATE TYPE "ScrapeJobStatus" AS ENUM ('pending', 'processing', 'completed', 'failed'); + +-- CreateTable +CREATE TABLE "content" ( + "id" TEXT NOT NULL, + "url" VARCHAR(500) NOT NULL, + "title" VARCHAR(255) NOT NULL, + "year" INTEGER, + "plot" TEXT, + "backdropUrl" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "content_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "genres" ( + "id" TEXT NOT NULL, + "name" VARCHAR(100) NOT NULL, + + CONSTRAINT "genres_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "content_genres" ( + "contentId" TEXT NOT NULL, + "genreId" TEXT NOT NULL, + + CONSTRAINT "content_genres_pkey" PRIMARY KEY ("contentId","genreId") +); + +-- CreateTable +CREATE TABLE "cast_members" ( + "id" TEXT NOT NULL, + "contentId" TEXT NOT NULL, + "name" VARCHAR(255) NOT NULL, + + CONSTRAINT "cast_members_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "scrape_jobs" ( + "id" TEXT NOT NULL, + "url" VARCHAR(500) NOT NULL, + "status" VARCHAR(20) NOT NULL DEFAULT 'pending', + "progress" INTEGER NOT NULL DEFAULT 0, + "step" VARCHAR(100), + "error" TEXT, + "result" JSONB, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "scrape_jobs_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "content_url_key" ON "content"("url"); + +-- CreateIndex +CREATE INDEX "content_url_idx" ON "content"("url"); + +-- CreateIndex +CREATE INDEX "content_title_idx" ON "content"("title"); + +-- CreateIndex +CREATE INDEX "content_year_idx" ON "content"("year"); + +-- CreateIndex +CREATE UNIQUE INDEX "genres_name_key" ON "genres"("name"); + +-- CreateIndex +CREATE INDEX "genres_name_idx" ON "genres"("name"); + +-- CreateIndex +CREATE INDEX "cast_members_contentId_idx" ON "cast_members"("contentId"); + +-- CreateIndex +CREATE INDEX "cast_members_name_idx" ON "cast_members"("name"); + +-- CreateIndex +CREATE INDEX "scrape_jobs_url_idx" ON "scrape_jobs"("url"); + +-- CreateIndex +CREATE INDEX "scrape_jobs_status_idx" ON "scrape_jobs"("status"); + +-- AddForeignKey +ALTER TABLE "content_genres" ADD CONSTRAINT "content_genres_contentId_fkey" FOREIGN KEY ("contentId") REFERENCES "content"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "content_genres" ADD CONSTRAINT "content_genres_genreId_fkey" FOREIGN KEY ("genreId") REFERENCES "genres"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "cast_members" ADD CONSTRAINT "cast_members_contentId_fkey" FOREIGN KEY ("contentId") REFERENCES "content"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250227000001_add_age_rating/migration.sql b/prisma/migrations/20250227000001_add_age_rating/migration.sql new file mode 100644 index 0000000..398daa2 --- /dev/null +++ b/prisma/migrations/20250227000001_add_age_rating/migration.sql @@ -0,0 +1,2 @@ +-- Add ageRating column to content table +ALTER TABLE "content" ADD COLUMN "ageRating" VARCHAR(10); diff --git a/prisma/migrations/20250227000002_add_content_type/migration.sql b/prisma/migrations/20250227000002_add_content_type/migration.sql new file mode 100644 index 0000000..3498609 --- /dev/null +++ b/prisma/migrations/20250227000002_add_content_type/migration.sql @@ -0,0 +1,5 @@ +-- Add type column to content table +ALTER TABLE "content" ADD COLUMN "type" VARCHAR(10) NOT NULL DEFAULT 'movie'; + +-- Create index for type field +CREATE INDEX "content_type_idx" ON "content"("type"); diff --git a/prisma/migrations/20250227000003_add_current_season/migration.sql b/prisma/migrations/20250227000003_add_current_season/migration.sql new file mode 100644 index 0000000..ab65ff0 --- /dev/null +++ b/prisma/migrations/20250227000003_add_current_season/migration.sql @@ -0,0 +1,5 @@ +-- Add currentSeason column to content table +ALTER TABLE "content" ADD COLUMN "currentSeason" INTEGER; + +-- Create index for currentSeason field +CREATE INDEX "content_current_season_idx" ON "content"("currentSeason"); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..99e4f20 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..de5f98c --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,103 @@ +// Prisma Schema for Netflix Scraper API +// Database: PostgreSQL + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// ============================================ +// Content Tables +// ============================================ + +/// Main content table for scraped Netflix data +model Content { + id String @id @default(uuid()) + url String @unique @db.VarChar(500) + title String @db.VarChar(255) + year Int? + plot String? @db.Text + backdropUrl String? @db.Text + ageRating String? @db.VarChar(10) + type String @default("movie") @db.VarChar(10) // movie or tvshow + currentSeason Int? // Current season number for TV shows + + // Relations + genres ContentGenre[] + castMembers CastMember[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([url]) + @@index([title]) + @@index([year]) + @@index([type]) + @@index([currentSeason]) + @@map("content") +} + +/// Genres lookup table +model Genre { + id String @id @default(uuid()) + name String @unique @db.VarChar(100) + + // Relations + contents ContentGenre[] + + @@index([name]) + @@map("genres") +} + +/// Content-Genre many-to-many relationship +model ContentGenre { + contentId String + genreId String + + content Content @relation(fields: [contentId], references: [id], onDelete: Cascade) + genre Genre @relation(fields: [genreId], references: [id], onDelete: Cascade) + + @@id([contentId, genreId]) + @@map("content_genres") +} + +/// Cast members for content +model CastMember { + id String @id @default(uuid()) + contentId String + name String @db.VarChar(255) + + content Content @relation(fields: [contentId], references: [id], onDelete: Cascade) + + @@index([contentId]) + @@index([name]) + @@map("cast_members") +} + +// ============================================ +// Job Queue Table (for async processing) +// ============================================ + +/// Scrape job queue +model ScrapeJob { + id String @id @default(uuid()) + url String @db.VarChar(500) + status String @default("pending") @db.VarChar(20) // pending, processing, completed, failed + progress Int @default(0) + step String? @db.VarChar(100) + error String? @db.Text + + // Result stored as JSON when completed + result Json? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([url]) + @@index([status]) + @@map("scrape_jobs") +} diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..a5b8506 --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,49 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +/** + * Seed script for initial data + * Run with: npx tsx prisma/seed.ts + */ +async function main() { + console.log('Seeding database...'); + + // Seed default genres + const genres = [ + 'Aksiyon', + 'Komedi', + 'Dram', + 'Korku', + 'Romantik', + 'Bilim Kurgu', + 'Gerilim', + 'Belgesel', + 'Animasyon', + 'Aile', + '18+', + '16+', + '13+', + '7+', + ]; + + for (const genreName of genres) { + await prisma.genre.upsert({ + where: { name: genreName }, + update: {}, + create: { name: genreName }, + }); + } + + console.log(`Seeded ${genres.length} genres`); + console.log('Seed completed successfully!'); +} + +main() + .catch((e) => { + console.error('Seed failed:', e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/scripts/dev-startup.sh b/scripts/dev-startup.sh new file mode 100755 index 0000000..4e137fa --- /dev/null +++ b/scripts/dev-startup.sh @@ -0,0 +1,34 @@ +#!/bin/sh +set -e + +echo "=== Netflix Scraper API Dev Startup ===" + +# Set DATABASE_URL from individual POSTGRES_* environment variables +# This overrides the dummy value set during Docker build +export DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" + +echo "Database URL configured: postgresql://${POSTGRES_USER}:***@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" + +# Wait for database to be ready +echo "Waiting for database..." +until nc -z $POSTGRES_HOST $POSTGRES_PORT; do + echo "Database not ready, waiting..." + sleep 2 +done +echo "Database is ready!" + +# Generate Prisma client +echo "Generating Prisma client..." +npx prisma generate + +# Run migrations +echo "Running database migrations..." +npx prisma migrate deploy + +# Run seed (optional) +echo "Running seed..." +npx tsx prisma/seed.ts || echo "Seed already run or not needed" + +# Start the application in dev mode +echo "Starting application in development mode..." +exec npm run dev diff --git a/scripts/startup.sh b/scripts/startup.sh new file mode 100755 index 0000000..71e107b --- /dev/null +++ b/scripts/startup.sh @@ -0,0 +1,30 @@ +#!/bin/sh +set -e + +echo "=== Netflix Scraper API Startup ===" + +# Set DATABASE_URL from individual POSTGRES_* environment variables +# This overrides the dummy value set during Docker build +export DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" + +echo "Database URL configured: postgresql://${POSTGRES_USER}:***@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" + +# Wait for database to be ready +echo "Waiting for database..." +until nc -z $POSTGRES_HOST $POSTGRES_PORT; do + echo "Database not ready, waiting..." + sleep 2 +done +echo "Database is ready!" + +# Run migrations +echo "Running database migrations..." +npx prisma migrate deploy + +# Run seed (optional, won't fail if already seeded) +echo "Running seed..." +npx tsx prisma/seed.ts || echo "Seed already run or not needed" + +# Start the application +echo "Starting application..." +exec node dist/index.js diff --git a/skills/brainstorming/SKILL.md b/skills/brainstorming/SKILL.md new file mode 100644 index 0000000..d4f31d3 --- /dev/null +++ b/skills/brainstorming/SKILL.md @@ -0,0 +1,104 @@ +--- +name: brainstorming +description: Clarify what to build before planning or implementation by exploring intent, constraints, trade-offs, and success criteria. Use when requests are ambiguous, have multiple valid interpretations, include phrases like "let's brainstorm" or "help me think through", or when feature scope needs refinement before writing an implementation plan. +--- + +# Brainstorming + +Guide collaborative brainstorming sessions that define WHAT to build before HOW to build it. Keep output short, decision-oriented, and validated with the user at each step. + +## Phase 0: Assess Clarity + +Decide whether brainstorming is necessary before asking discovery questions. + +Signals to skip brainstorming and proceed: +- Requirements include concrete acceptance criteria. +- Expected behavior and boundaries are explicit. +- Task is a straightforward bug fix or well-scoped change. + +If clarity is already high, say: +- "Requirements look clear. Proceed directly to planning or implementation." + +## Phase 1: Understand the Idea + +Ask one question at a time. Prefer multiple-choice when natural options exist. + +Question order: +1. Purpose: problem and motivation. +2. Users: who uses it and in what context. +3. Constraints: technical, timeline, dependencies. +4. Success: how "good" is measured. +5. Edge cases: failures, exclusions, non-goals. +6. Existing patterns: similar feature to follow. + +Validate assumptions explicitly: +- "I assume users are authenticated. Correct?" + +Exit this phase when the request is clear or the user says "proceed". + +## Phase 2: Explore Approaches + +Provide 2-3 options with explicit trade-offs and a recommendation. + +Use this structure: + +### Approach A: +<2-3 sentence summary> + +Pros: +- +- + +Cons: +- +- + +Best when: +- + +Apply YAGNI: +- Prefer the simplest approach that solves the current problem. +- Defer speculative complexity. + +## Phase 3: Capture Decisions + +Write a concise brainstorm note to: +- `docs/brainstorms/YYYY-MM-DD--brainstorm.md` + +Use this format: + +```markdown +--- +date: YYYY-MM-DD +topic: +--- + +# + +## What We're Building +<1-2 short paragraphs> + +## Why This Approach + + +## Key Decisions +- : +- : + +## Open Questions +- + +## Next Steps +-> `/workflows:plan` +``` + +## Phase 4: Handoff + +Offer exactly one clear next action: +1. Proceed to planning. +2. Refine assumptions. +3. Pause and resume later. + +Keep each response section short (about 200-300 words max), then confirm alignment: +- "Does this match your intent?" +- "Any adjustments before we continue?" diff --git a/skills/brainstorming/agents/openai.yaml b/skills/brainstorming/agents/openai.yaml new file mode 100644 index 0000000..67e235b --- /dev/null +++ b/skills/brainstorming/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Brainstorming" + short_description: "Clarify what to build before planning or implementation." + default_prompt: "Use this skill to clarify ambiguous requests, compare approaches, and capture decisions before planning." diff --git a/src/config/database.ts b/src/config/database.ts new file mode 100644 index 0000000..2c444d1 --- /dev/null +++ b/src/config/database.ts @@ -0,0 +1,63 @@ +import { PrismaClient } from '@prisma/client'; +import { getDatabaseUrl, env } from './env.js'; +import logger from '../utils/logger.js'; + +// Set database URL for Prisma +process.env.DATABASE_URL = getDatabaseUrl(); + +/** + * Prisma Client singleton + * Handles connection pooling and retries + */ +const globalForPrisma = globalThis as unknown as { + prisma: PrismaClient | undefined; +}; + +export const prisma = + globalForPrisma.prisma ?? + new PrismaClient({ + log: + env.NODE_ENV === 'development' + ? ['query', 'error', 'warn'] + : ['error'], + }); + +if (env.NODE_ENV !== 'production') { + globalForPrisma.prisma = prisma; +} + +/** + * Connect to database with retry logic + */ +export async function connectDatabase(retries = 5, delay = 2000): Promise { + for (let i = 0; i < retries; i++) { + try { + await prisma.$connect(); + logger.info('Database connected successfully', { + host: env.POSTGRES_HOST, + database: env.POSTGRES_DB, + }); + return; + } catch (error) { + logger.warn(`Database connection attempt ${i + 1}/${retries} failed`, { + error: error instanceof Error ? error.message : 'Unknown error', + }); + + if (i < retries - 1) { + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + } + + throw new Error('Failed to connect to database after maximum retries'); +} + +/** + * Disconnect from database + */ +export async function disconnectDatabase(): Promise { + await prisma.$disconnect(); + logger.info('Database disconnected'); +} + +export default prisma; diff --git a/src/config/env.ts b/src/config/env.ts new file mode 100644 index 0000000..23b9580 --- /dev/null +++ b/src/config/env.ts @@ -0,0 +1,77 @@ +import { z } from 'zod'; + +/** + * Environment variable schema with validation + * Fails fast on startup if required variables are missing + */ +const envSchema = z.object({ + // Server + NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), + PORT: z.string().transform(Number).pipe(z.number().positive()).default('3000'), + + // PostgreSQL + POSTGRES_HOST: z.string().min(1), + POSTGRES_PORT: z.string().transform(Number).pipe(z.number().positive()).default('5432'), + POSTGRES_USER: z.string().min(1), + POSTGRES_PASSWORD: z.string().min(1), + POSTGRES_DB: z.string().min(1), + + // Redis + REDIS_HOST: z.string().min(1), + REDIS_PORT: z.string().transform(Number).pipe(z.number().positive()).default('6379'), + REDIS_TTL_SECONDS: z.string().transform(Number).pipe(z.number().positive()).default('604800'), // 7 days + + // Rate Limiting + RATE_LIMIT_WINDOW_MS: z.string().transform(Number).pipe(z.number().positive()).default('60000'), // 1 minute + RATE_LIMIT_MAX_REQUESTS: z.string().transform(Number).pipe(z.number().positive()).default('30'), + + // API Keys (named keys for different frontends) + API_KEY_WEB: z.string().min(1), + API_KEY_MOBILE: z.string().min(1), + API_KEY_ADMIN: z.string().min(1), + + // TMDB API + TMDB_API_KEY: z.string().min(1), + TMDB_ACCESS_TOKEN: z.string().min(1), +}); + +export type EnvConfig = z.infer; + +/** + * Parse and validate environment variables + * Throws error on validation failure + */ +function parseEnv(): EnvConfig { + const result = envSchema.safeParse(process.env); + + if (!result.success) { + const errors = result.error.issues.map( + (issue) => ` - ${issue.path.join('.')}: ${issue.message}` + ); + throw new Error( + `Environment validation failed:\n${errors.join('\n')}\n\nPlease check your .env file.` + ); + } + + return result.data; +} + +export const env = parseEnv(); + +/** + * Get all valid API keys as a Set for O(1) lookup + */ +export function getValidApiKeys(): Set { + return new Set([ + env.API_KEY_WEB, + env.API_KEY_MOBILE, + env.API_KEY_ADMIN, + ]); +} + +/** + * Get database connection URL + */ +export function getDatabaseUrl(): string { + return `postgresql://${env.POSTGRES_USER}:${env.POSTGRES_PASSWORD}@${env.POSTGRES_HOST}:${env.POSTGRES_PORT}/${env.POSTGRES_DB}`; +} diff --git a/src/config/redis.ts b/src/config/redis.ts new file mode 100644 index 0000000..3b32f7f --- /dev/null +++ b/src/config/redis.ts @@ -0,0 +1,66 @@ +import Redis from 'ioredis'; +import { env } from './env.js'; +import logger from '../utils/logger.js'; + +/** + * Redis Client singleton + */ +const globalForRedis = globalThis as unknown as { + redis: Redis | undefined; +}; + +export const redis = + globalForRedis.redis ?? + new Redis({ + host: env.REDIS_HOST, + port: env.REDIS_PORT, + retryStrategy: (times: number) => { + if (times > 5) { + logger.error('Redis connection failed after 5 retries'); + return null; + } + const delay = Math.min(times * 1000, 5000); + logger.warn(`Redis retrying connection in ${delay}ms`, { attempt: times }); + return delay; + }, + maxRetriesPerRequest: 3, + }); + +if (env.NODE_ENV !== 'production') { + globalForRedis.redis = redis; +} + +redis.on('connect', () => { + logger.info('Redis connected successfully', { + host: env.REDIS_HOST, + port: env.REDIS_PORT, + }); +}); + +redis.on('error', (error) => { + logger.error('Redis connection error', { + error: error.message, + }); +}); + +/** + * Check Redis connection + */ +export async function checkRedisConnection(): Promise { + try { + const result = await redis.ping(); + return result === 'PONG'; + } catch { + return false; + } +} + +/** + * Disconnect from Redis + */ +export async function disconnectRedis(): Promise { + await redis.quit(); + logger.info('Redis disconnected'); +} + +export default redis; diff --git a/src/config/socket.ts b/src/config/socket.ts new file mode 100644 index 0000000..23c2586 --- /dev/null +++ b/src/config/socket.ts @@ -0,0 +1,131 @@ +import { Server as HttpServer } from 'http'; +import { Server, Socket } from 'socket.io'; +import logger from '../utils/logger.js'; + +/** + * Socket.IO Server singleton + */ +let io: Server | null = null; + +export interface SocketData { + subscribedJobs: Set; +} + +/** + * Initialize Socket.IO server + */ +export function initializeSocket(httpServer: HttpServer): Server { + io = new Server(httpServer, { + cors: { + origin: '*', // Configure based on your frontend domains + methods: ['GET', 'POST'], + }, + transports: ['websocket', 'polling'], + }); + + io.on('connection', (socket: Socket) => { + logger.info('Client connected', { socketId: socket.id }); + + // Initialize socket data + (socket.data as SocketData).subscribedJobs = new Set(); + + // Handle job subscription + socket.on('job:subscribe', (jobId: string) => { + (socket.data as SocketData).subscribedJobs.add(jobId); + socket.join(`job:${jobId}`); + logger.debug('Client subscribed to job', { socketId: socket.id, jobId }); + }); + + // Handle job unsubscription + socket.on('job:unsubscribe', (jobId: string) => { + (socket.data as SocketData).subscribedJobs.delete(jobId); + socket.leave(`job:${jobId}`); + logger.debug('Client unsubscribed from job', { socketId: socket.id, jobId }); + }); + + socket.on('disconnect', () => { + logger.info('Client disconnected', { socketId: socket.id }); + }); + }); + + logger.info('Socket.IO server initialized'); + return io; +} + +/** + * Get Socket.IO server instance + */ +export function getSocketIO(): Server { + if (!io) { + throw new Error('Socket.IO not initialized. Call initializeSocket first.'); + } + return io; +} + +/** + * Emit job progress to subscribers + */ +export function emitJobProgress( + jobId: string, + progress: number, + status: string, + step: string +): void { + if (io) { + io.to(`job:${jobId}`).emit('job:progress', { + jobId, + progress, + status, + step, + }); + } +} + +/** + * Emit job completed event + */ +export function emitJobCompleted( + jobId: string, + data: unknown, + source: string +): void { + if (io) { + io.to(`job:${jobId}`).emit('job:completed', { + jobId, + data, + source, + }); + } +} + +/** + * Emit job error event + */ +export function emitJobError( + jobId: string, + error: { code: string; message: string } +): void { + if (io) { + io.to(`job:${jobId}`).emit('job:error', { + jobId, + error, + }); + } +} + +/** + * Close Socket.IO server + */ +export async function closeSocket(): Promise { + if (io) { + await new Promise((resolve) => { + io!.close(() => { + logger.info('Socket.IO server closed'); + resolve(); + }); + }); + io = null; + } +} + +export default io; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..ac17403 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,112 @@ +import express from 'express'; +import { createServer } from 'http'; +import { env, getDatabaseUrl } from './config/env.js'; +import { connectDatabase, disconnectDatabase } from './config/database.js'; +import { disconnectRedis } from './config/redis.js'; +import { initializeSocket, closeSocket } from './config/socket.js'; +import { rateLimiter } from './middleware/rateLimit.middleware.js'; +import { errorHandler, notFoundHandler } from './middleware/error.middleware.js'; +import apiRoutes from './routes/api.routes.js'; +import tmdbRoutes from './routes/tmdb.routes.js'; +import healthRoutes from './routes/health.routes.js'; +import logger from './utils/logger.js'; + +// Set DATABASE_URL for Prisma +process.env.DATABASE_URL = getDatabaseUrl(); + +/** + * Application entry point + */ +async function main() { + const app = express(); + const httpServer = createServer(app); + + // Initialize Socket.IO + initializeSocket(httpServer); + + // Middleware + app.use(express.json({ limit: '10mb' })); + app.use(express.urlencoded({ extended: true })); + + // Apply general rate limiting + app.use(rateLimiter); + + // Request logging middleware + app.use((req, res, next) => { + logger.info('Incoming request', { + method: req.method, + path: req.path, + ip: req.ip, + userAgent: req.headers['user-agent'], + }); + next(); + }); + + // Health check routes (no auth required) + app.use(healthRoutes); + + // API routes + app.use('/api', apiRoutes); + app.use('/api/tmdb', tmdbRoutes); + + // 404 handler + app.use(notFoundHandler); + + // Error handler + app.use(errorHandler); + + // Connect to database with retry + await connectDatabase(); + + // Start server + httpServer.listen(env.PORT, () => { + logger.info('Server started', { + port: env.PORT, + env: env.NODE_ENV, + }); + }); + + // Graceful shutdown handlers + const gracefulShutdown = async (signal: string) => { + logger.info(`Received ${signal}, starting graceful shutdown`); + + // Close server + httpServer.close(() => { + logger.info('HTTP server closed'); + }); + + // Close connections + await closeSocket(); + await disconnectRedis(); + await disconnectDatabase(); + + logger.info('Graceful shutdown completed'); + process.exit(0); + }; + + process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); + process.on('SIGINT', () => gracefulShutdown('SIGINT')); + + // Handle uncaught exceptions + process.on('uncaughtException', (error) => { + logger.error('Uncaught exception', { + error: error.message, + stack: error.stack, + }); + process.exit(1); + }); + + process.on('unhandledRejection', (reason) => { + logger.error('Unhandled rejection', { + reason: reason instanceof Error ? reason.message : String(reason), + }); + }); +} + +main().catch((error) => { + logger.error('Application startup failed', { + error: error.message, + stack: error.stack, + }); + process.exit(1); +}); diff --git a/src/middleware/auth.middleware.ts b/src/middleware/auth.middleware.ts new file mode 100644 index 0000000..75d921e --- /dev/null +++ b/src/middleware/auth.middleware.ts @@ -0,0 +1,73 @@ +import { Request, Response, NextFunction } from 'express'; +import { getValidApiKeys } from '../config/env.js'; +import logger from '../utils/logger.js'; +import type { ApiResponse } from '../types/index.js'; + +/** + * API Key Authentication Middleware + * Validates API key from X-API-Key header + */ +export function authMiddleware( + req: Request, + res: Response, + next: NextFunction +): void { + const apiKey = req.headers['x-api-key'] as string | undefined; + + if (!apiKey) { + const response: ApiResponse = { + success: false, + error: { + code: 'MISSING_API_KEY', + message: 'API key is required. Include X-API-Key header.', + }, + }; + + logger.warn('Request missing API key', { + ip: req.ip, + path: req.path, + }); + + res.status(401).json(response); + return; + } + + const validKeys = getValidApiKeys(); + + if (!validKeys.has(apiKey)) { + const response: ApiResponse = { + success: false, + error: { + code: 'INVALID_API_KEY', + message: 'Invalid API key provided.', + }, + }; + + logger.warn('Invalid API key attempt', { + ip: req.ip, + path: req.path, + keyPrefix: apiKey.substring(0, 8) + '...', + }); + + res.status(403).json(response); + return; + } + + // Valid API key, proceed + next(); +} + +/** + * Optional: Identify which client made the request + */ +export function identifyClient(apiKey: string): string { + const { env } = require('../config/env.js'); + + if (apiKey === env.API_KEY_WEB) return 'web'; + if (apiKey === env.API_KEY_MOBILE) return 'mobile'; + if (apiKey === env.API_KEY_ADMIN) return 'admin'; + + return 'unknown'; +} + +export default authMiddleware; diff --git a/src/middleware/error.middleware.ts b/src/middleware/error.middleware.ts new file mode 100644 index 0000000..4d7eaa9 --- /dev/null +++ b/src/middleware/error.middleware.ts @@ -0,0 +1,50 @@ +import { Request, Response, NextFunction } from 'express'; +import logger from '../utils/logger.js'; +import type { ApiResponse } from '../types/index.js'; + +/** + * Global Error Handler Middleware + */ +export function errorHandler( + error: Error, + req: Request, + res: Response, + _next: NextFunction +): void { + logger.error('Unhandled error', { + error: error.message, + stack: error.stack, + path: req.path, + method: req.method, + }); + + const response: ApiResponse = { + success: false, + error: { + code: 'INTERNAL_ERROR', + message: 'An unexpected error occurred. Please try again later.', + }, + }; + + res.status(500).json(response); +} + +/** + * 404 Not Found Handler + */ +export function notFoundHandler( + req: Request, + res: Response +): void { + const response: ApiResponse = { + success: false, + error: { + code: 'NOT_FOUND', + message: `Endpoint ${req.method} ${req.path} not found`, + }, + }; + + res.status(404).json(response); +} + +export default errorHandler; diff --git a/src/middleware/rateLimit.middleware.ts b/src/middleware/rateLimit.middleware.ts new file mode 100644 index 0000000..4aa53c2 --- /dev/null +++ b/src/middleware/rateLimit.middleware.ts @@ -0,0 +1,87 @@ +import rateLimit from 'express-rate-limit'; +import { env } from '../config/env.js'; +import logger from '../utils/logger.js'; +import type { ApiResponse } from '../types/index.js'; + +/** + * Rate Limiter Configuration + * Limits requests per IP within a time window + */ +export const rateLimiter = rateLimit({ + windowMs: env.RATE_LIMIT_WINDOW_MS, // Time window in milliseconds + max: env.RATE_LIMIT_MAX_REQUESTS, // Max requests per window per IP + standardHeaders: true, // Return rate limit info in RateLimit-* headers + legacyHeaders: false, // Disable X-RateLimit-* headers + + // Custom key generator (use IP + API key for more granular limiting) + keyGenerator: (req) => { + const apiKey = req.headers['x-api-key'] as string | undefined; + return `${req.ip}:${apiKey || 'no-key'}`; + }, + + // Custom handler for rate limit exceeded + handler: (req, res) => { + const response: ApiResponse = { + success: false, + error: { + code: 'RATE_LIMIT_EXCEEDED', + message: `Too many requests. Maximum ${env.RATE_LIMIT_MAX_REQUESTS} requests per ${env.RATE_LIMIT_WINDOW_MS / 1000} seconds.`, + details: { + retryAfter: Math.ceil(env.RATE_LIMIT_WINDOW_MS / 1000), + }, + }, + }; + + logger.warn('Rate limit exceeded', { + ip: req.ip, + path: req.path, + maxRequests: env.RATE_LIMIT_MAX_REQUESTS, + windowMs: env.RATE_LIMIT_WINDOW_MS, + }); + + res.status(429).json(response); + }, + + // Skip rate limiting for health checks + skip: (req) => { + return req.path === '/health' || req.path === '/ready'; + }, +}); + +/** + * Stricter rate limiter for scraping endpoints + * Prevents abuse of Netflix scraping + */ +export const scrapeRateLimiter = rateLimit({ + windowMs: 60 * 1000, // 1 minute + max: 10, // Only 10 scrape requests per minute + standardHeaders: true, + legacyHeaders: false, + + keyGenerator: (req) => { + const apiKey = req.headers['x-api-key'] as string | undefined; + return `scrape:${req.ip}:${apiKey || 'no-key'}`; + }, + + handler: (req, res) => { + const response: ApiResponse = { + success: false, + error: { + code: 'SCRAPE_RATE_LIMIT_EXCEEDED', + message: 'Too many scrape requests. Please wait before trying again.', + details: { + retryAfter: 60, + }, + }, + }; + + logger.warn('Scrape rate limit exceeded', { + ip: req.ip, + path: req.path, + }); + + res.status(429).json(response); + }, +}); + +export default rateLimiter; diff --git a/src/middleware/validation.middleware.ts b/src/middleware/validation.middleware.ts new file mode 100644 index 0000000..a49562d --- /dev/null +++ b/src/middleware/validation.middleware.ts @@ -0,0 +1,93 @@ +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; +import type { ApiResponse, GetInfoRequest } from '../types/index.js'; + +/** + * Validation schema for /api/getinfo endpoint + */ +const getInfoSchema = z.object({ + url: z.string().url('Invalid URL format').refine((url) => { + // Validate Netflix URL + try { + const parsedUrl = new URL(url); + const validHosts = [ + 'www.netflix.com', + 'netflix.com', + 'www.netflix.com.tr', + 'netflix.com.tr', + ]; + const hasTitlePath = /\/title\/\d+/.test(url); + return validHosts.includes(parsedUrl.hostname) && hasTitlePath; + } catch { + return false; + } + }, 'URL must be a valid Netflix title URL (e.g., https://www.netflix.com/tr/title/81616256)'), +}); + +/** + * Validate request body for /api/getinfo + */ +export function validateGetInfo( + req: Request, + res: Response, + next: NextFunction +): void { + const result = getInfoSchema.safeParse(req.body); + + if (!result.success) { + const errors = result.error.issues.map((issue) => ({ + field: issue.path.join('.'), + message: issue.message, + })); + + const response: ApiResponse = { + success: false, + error: { + code: 'VALIDATION_ERROR', + message: 'Invalid request parameters', + details: { errors }, + }, + }; + + res.status(400).json(response); + return; + } + + // Attach validated data to request + (req as Request & { validated: GetInfoRequest }).validated = result.data; + next(); +} + +/** + * Generic validation middleware factory + */ +export function validateBody( + schema: T +): (req: Request, res: Response, next: NextFunction) => void { + return (req, res, next) => { + const result = schema.safeParse(req.body); + + if (!result.success) { + const errors = result.error.issues.map((issue) => ({ + field: issue.path.join('.'), + message: issue.message, + })); + + const response: ApiResponse = { + success: false, + error: { + code: 'VALIDATION_ERROR', + message: 'Invalid request parameters', + details: { errors }, + }, + }; + + res.status(400).json(response); + return; + } + + next(); + }; +} + +export default validateGetInfo; diff --git a/src/routes/api.routes.ts b/src/routes/api.routes.ts new file mode 100644 index 0000000..25dc49e --- /dev/null +++ b/src/routes/api.routes.ts @@ -0,0 +1,234 @@ +import { Router, Request, Response } from 'express'; +import { z } from 'zod'; +import { authMiddleware } from '../middleware/auth.middleware.js'; +import { scrapeRateLimiter } from '../middleware/rateLimit.middleware.js'; +import { validateGetInfo } from '../middleware/validation.middleware.js'; +import { JobService } from '../services/job.service.js'; +import { ContentService } from '../services/content.service.js'; +import type { ApiResponse, GetInfoRequest, GetInfoResponse } from '../types/index.js'; + +const router = Router(); +const listContentSchema = z.object({ + type: z.enum(['movie', 'tvshow']).optional(), + limit: z.coerce.number().int().min(1).max(100).optional(), +}); + +/** + * POST /api/getinfo + * Get content information from Netflix URL + * + * Request body: { url: string } + * Headers: X-API-Key: + * + * Response: { success: boolean, data?: ContentData, error?: ApiError } + */ +router.post( + '/getinfo', + authMiddleware, + scrapeRateLimiter, + validateGetInfo, + async ( + req: Request & { validated: GetInfoRequest }, + res: Response> + ) => { + const { url } = req.validated; + + try { + // Process synchronously (hybrid: cache -> db -> netflix) + const result = await JobService.processSync(url); + + const response: ApiResponse = { + success: true, + data: result.data, + }; + + res.json(response); + } catch (error) { + const response: ApiResponse = { + success: false, + error: { + code: 'SCRAPE_ERROR', + message: + error instanceof Error ? error.message : 'Failed to scrape content', + }, + }; + + res.status(500).json(response); + } + } +); + +/** + * GET /api/content + * List content already stored in DB + * + * Query params: type?: movie|tvshow, limit?: 1-100 + * Headers: X-API-Key: + */ +router.get( + '/content', + authMiddleware, + async ( + req: Request, + res: Response> + ) => { + const validation = listContentSchema.safeParse(req.query); + + if (!validation.success) { + const errors = validation.error.issues.map((issue) => ({ + field: issue.path.join('.'), + message: issue.message, + })); + + const response: ApiResponse = { + success: false, + error: { + code: 'VALIDATION_ERROR', + message: 'Invalid query parameters', + details: { errors }, + }, + }; + res.status(400).json(response); + return; + } + + try { + const content = await ContentService.list({ + type: validation.data.type, + limit: validation.data.limit ?? 100, + }); + + const response: ApiResponse = { + success: true, + data: content.map((item) => ContentService.toApiResponse(item)), + }; + + res.json(response); + } catch (error) { + const response: ApiResponse = { + success: false, + error: { + code: 'CONTENT_LIST_ERROR', + message: + error instanceof Error ? error.message : 'Failed to fetch content', + }, + }; + res.status(500).json(response); + } + } +); + +/** + * POST /api/getinfo/async + * Create async job for content scraping + * + * Request body: { url: string } + * Headers: X-API-Key: + * + * Response: { success: boolean, data?: { jobId: string }, error?: ApiError } + */ +router.post( + '/getinfo/async', + authMiddleware, + scrapeRateLimiter, + validateGetInfo, + async ( + req: Request & { validated: GetInfoRequest }, + res: Response> + ) => { + const { url } = req.validated; + + try { + // Create job + const job = await JobService.create(url); + + // Start processing in background + JobService.process(job.id).catch((err) => { + console.error('Job processing error:', err); + }); + + const response: ApiResponse<{ jobId: string; status: string }> = { + success: true, + data: { + jobId: job.id, + status: job.status, + }, + }; + + res.status(202).json(response); + } catch (error) { + const response: ApiResponse<{ jobId: string; status: string }> = { + success: false, + error: { + code: 'JOB_CREATE_ERROR', + message: + error instanceof Error ? error.message : 'Failed to create job', + }, + }; + + res.status(500).json(response); + } + } +); + +/** + * GET /api/jobs/:jobId + * Get job status + * + * Headers: X-API-Key: + */ +router.get( + '/jobs/:jobId', + authMiddleware, + async (req: Request, res: Response) => { + const { jobId } = req.params; + if (!jobId) { + const response: ApiResponse = { + success: false, + error: { + code: 'VALIDATION_ERROR', + message: 'jobId is required', + }, + }; + res.status(400).json(response); + return; + } + + try { + const job = await JobService.getById(jobId); + + if (!job) { + const response: ApiResponse = { + success: false, + error: { + code: 'JOB_NOT_FOUND', + message: 'Job not found', + }, + }; + + res.status(404).json(response); + return; + } + + const response = { + success: true, + data: job, + }; + + res.json(response); + } catch (error) { + const response: ApiResponse = { + success: false, + error: { + code: 'JOB_FETCH_ERROR', + message: + error instanceof Error ? error.message : 'Failed to fetch job', + }, + }; + + res.status(500).json(response); + } + } +); + +export default router; diff --git a/src/routes/health.routes.ts b/src/routes/health.routes.ts new file mode 100644 index 0000000..7321a83 --- /dev/null +++ b/src/routes/health.routes.ts @@ -0,0 +1,54 @@ +import { Router, Request, Response } from 'express'; +import { checkRedisConnection } from '../config/redis.js'; +import prisma from '../config/database.js'; +import { env } from '../config/env.js'; + +const router = Router(); + +/** + * GET /health + * Basic health check endpoint + */ +router.get('/health', (_req: Request, res: Response) => { + res.status(200).json({ + status: 'ok', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + }); +}); + +/** + * GET /ready + * Readiness check - verifies all dependencies are available + */ +router.get('/ready', async (_req: Request, res: Response) => { + const checks = { + database: false, + redis: false, + }; + + // Check database + try { + await prisma.$queryRaw`SELECT 1`; + checks.database = true; + } catch (error) { + console.error('Database health check failed:', error); + } + + // Check Redis + checks.redis = await checkRedisConnection(); + + const allHealthy = checks.database && checks.redis; + + res.status(allHealthy ? 200 : 503).json({ + status: allHealthy ? 'ready' : 'not_ready', + timestamp: new Date().toISOString(), + checks: { + database: checks.database ? 'healthy' : 'unhealthy', + redis: checks.redis ? 'healthy' : 'unhealthy', + }, + env: env.NODE_ENV, + }); +}); + +export default router; diff --git a/src/routes/tmdb.routes.ts b/src/routes/tmdb.routes.ts new file mode 100644 index 0000000..6ed13b7 --- /dev/null +++ b/src/routes/tmdb.routes.ts @@ -0,0 +1,222 @@ +import { Router, Request, Response } from 'express'; +import { z } from 'zod'; +import { authMiddleware } from '../middleware/auth.middleware.js'; +import { scrapeRateLimiter } from '../middleware/rateLimit.middleware.js'; +import { TmdbService } from '../services/tmdb.service.js'; +import type { + ApiResponse, + TmdbSearchResponse, +} from '../types/index.js'; + +const router = Router(); + +/** + * Validation schema for TMDB search + */ +const tmdbSearchSchema = z.object({ + query: z.string().trim().min(1, 'Query must be at least 1 character').max(200, 'Query must be at most 200 characters'), + year: z.coerce.number().int().min(1900).max(new Date().getFullYear() + 10).optional(), + type: z.enum(['movie', 'tv', 'multi']).optional(), + seasonYear: z.coerce.number().int().min(1900).max(new Date().getFullYear() + 10).optional(), + seasonNumber: z.coerce.number().int().min(1).max(100).optional(), +}); + +/** + * POST /api/tmdb/search + * Search for movies and TV shows using TMDB API + * + * Request body: { query: string, year?: number, type?: 'movie' | 'tv' | 'multi' } + * Headers: X-API-Key: + * + * Response: { success: boolean, data?: TmdbSearchResponse, error?: ApiError } + */ +router.post( + '/search', + authMiddleware, + scrapeRateLimiter, + async ( + req: Request, + res: Response> + ) => { + // Validate request body + const result = tmdbSearchSchema.safeParse(req.body); + + if (!result.success) { + const errors = result.error.issues.map((issue) => ({ + field: issue.path.join('.'), + message: issue.message, + })); + + const response: ApiResponse = { + success: false, + error: { + code: 'VALIDATION_ERROR', + message: 'Invalid request parameters', + details: { errors }, + }, + }; + res.status(400).json(response); + return; + } + + const { query, year, type, seasonYear, seasonNumber } = result.data; + + try { + const searchResult = await TmdbService.search({ + query, + year, + type: type || 'multi', + seasonYear, + seasonNumber, + }); + + const response: ApiResponse = { + success: true, + data: searchResult, + }; + + res.json(response); + } catch (error) { + const response: ApiResponse = { + success: false, + error: { + code: 'TMDB_ERROR', + message: + error instanceof Error ? error.message : 'Failed to search TMDB', + }, + }; + + res.status(500).json(response); + } + } +); + +/** + * POST /api/tmdb/search/movie + * Search for movies only + */ +router.post( + '/search/movie', + authMiddleware, + scrapeRateLimiter, + async ( + req: Request, + res: Response> + ) => { + const movieSearchSchema = z.object({ + query: z.string().trim().min(1).max(200), + year: z.coerce.number().int().min(1900).max(new Date().getFullYear() + 10).optional(), + }); + + const result = movieSearchSchema.safeParse(req.body); + + if (!result.success) { + const errors = result.error.issues.map((issue) => ({ + field: issue.path.join('.'), + message: issue.message, + })); + + const response: ApiResponse = { + success: false, + error: { + code: 'VALIDATION_ERROR', + message: 'Invalid request parameters', + details: { errors }, + }, + }; + res.status(400).json(response); + return; + } + + const { query, year } = result.data; + + try { + const searchResult = await TmdbService.searchMovies(query, year); + + const response: ApiResponse = { + success: true, + data: searchResult, + }; + + res.json(response); + } catch (error) { + const response: ApiResponse = { + success: false, + error: { + code: 'TMDB_ERROR', + message: + error instanceof Error ? error.message : 'Failed to search movies', + }, + }; + + res.status(500).json(response); + } + } +); + +/** + * POST /api/tmdb/search/tv + * Search for TV shows only + */ +router.post( + '/search/tv', + authMiddleware, + scrapeRateLimiter, + async ( + req: Request, + res: Response> + ) => { + const tvSearchSchema = z.object({ + query: z.string().trim().min(1).max(200), + year: z.coerce.number().int().min(1900).max(new Date().getFullYear() + 10).optional(), + seasonYear: z.coerce.number().int().min(1900).max(new Date().getFullYear() + 10).optional(), + seasonNumber: z.coerce.number().int().min(1).max(100).optional(), + }); + + const result = tvSearchSchema.safeParse(req.body); + + if (!result.success) { + const errors = result.error.issues.map((issue) => ({ + field: issue.path.join('.'), + message: issue.message, + })); + + const response: ApiResponse = { + success: false, + error: { + code: 'VALIDATION_ERROR', + message: 'Invalid request parameters', + details: { errors }, + }, + }; + res.status(400).json(response); + return; + } + + const { query, year, seasonYear, seasonNumber } = result.data; + + try { + const searchResult = await TmdbService.searchTv(query, year, seasonNumber, seasonYear); + + const response: ApiResponse = { + success: true, + data: searchResult, + }; + + res.json(response); + } catch (error) { + const response: ApiResponse = { + success: false, + error: { + code: 'TMDB_ERROR', + message: + error instanceof Error ? error.message : 'Failed to search TV shows', + }, + }; + + res.status(500).json(response); + } + } +); + +export default router; diff --git a/src/services/cache.service.ts b/src/services/cache.service.ts new file mode 100644 index 0000000..06ed479 --- /dev/null +++ b/src/services/cache.service.ts @@ -0,0 +1,146 @@ +import redis from '../config/redis.js'; +import { env } from '../config/env.js'; +import logger from '../utils/logger.js'; +import type { GetInfoResponse, CacheEntry, DataSource } from '../types/index.js'; + +/** + * Cache key prefix for Netflix content + */ +const CACHE_PREFIX = 'netflix:content:'; + +/** + * Generate cache key from URL + */ +function getCacheKey(url: string): string { + // Use URL hash or title ID as key + const titleId = url.match(/\/title\/(\d+)/)?.[1] || url; + return `${CACHE_PREFIX}${titleId}`; +} + +/** + * Cache Service for Redis operations + * Handles caching with TTL support + */ +export class CacheService { + /** + * Get cached content by URL + */ + static async get(url: string): Promise { + const key = getCacheKey(url); + + try { + const cached = await redis.get(key); + + if (!cached) { + logger.debug('Cache miss', { url }); + return null; + } + + logger.debug('Cache hit', { url }); + const entry: CacheEntry = JSON.parse(cached); + return entry.data; + } catch (error) { + logger.error('Cache get error', { + url, + error: error instanceof Error ? error.message : 'Unknown error', + }); + return null; + } + } + + /** + * Set cache entry with TTL + */ + static async set(url: string, data: GetInfoResponse): Promise { + const key = getCacheKey(url); + const ttl = env.REDIS_TTL_SECONDS; + + const entry: CacheEntry = { + data, + cachedAt: Date.now(), + ttl, + }; + + try { + await redis.setex(key, ttl, JSON.stringify(entry)); + logger.debug('Cache set', { url, ttl }); + } catch (error) { + logger.error('Cache set error', { + url, + error: error instanceof Error ? error.message : 'Unknown error', + }); + } + } + + /** + * Delete cached content + */ + static async delete(url: string): Promise { + const key = getCacheKey(url); + + try { + await redis.del(key); + logger.debug('Cache deleted', { url }); + } catch (error) { + logger.error('Cache delete error', { + url, + error: error instanceof Error ? error.message : 'Unknown error', + }); + } + } + + /** + * Check if cache exists + */ + static async exists(url: string): Promise { + const key = getCacheKey(url); + + try { + const result = await redis.exists(key); + return result === 1; + } catch (error) { + logger.error('Cache exists check error', { + url, + error: error instanceof Error ? error.message : 'Unknown error', + }); + return false; + } + } + + /** + * Get cache TTL remaining + */ + static async getTTL(url: string): Promise { + const key = getCacheKey(url); + + try { + return await redis.ttl(key); + } catch (error) { + logger.error('Cache TTL check error', { + url, + error: error instanceof Error ? error.message : 'Unknown error', + }); + return -1; + } + } + + /** + * Clear all Netflix content cache + */ + static async clearAll(): Promise { + try { + const keys = await redis.keys(`${CACHE_PREFIX}*`); + + if (keys.length > 0) { + await redis.del(...keys); + logger.info('Cache cleared', { count: keys.length }); + } + } catch (error) { + logger.error('Cache clear error', { + error: error instanceof Error ? error.message : 'Unknown error', + }); + } + } +} + +export default CacheService; diff --git a/src/services/content.service.ts b/src/services/content.service.ts new file mode 100644 index 0000000..685c0a3 --- /dev/null +++ b/src/services/content.service.ts @@ -0,0 +1,239 @@ +import prisma from '../config/database.js'; +import type { ContentData, ScraperResult, GetInfoResponse } from '../types/index.js'; + +/** + * Content Service for database operations + */ +export class ContentService { + /** + * List content items from database + */ + static async list(options?: { + type?: 'movie' | 'tvshow'; + limit?: number; + }): Promise { + const content = await prisma.content.findMany({ + where: options?.type ? { type: options.type } : undefined, + include: { + genres: { + include: { + genre: true, + }, + }, + castMembers: { + orderBy: { name: 'asc' }, + }, + }, + orderBy: { createdAt: 'desc' }, + take: options?.limit, + }); + + return content.map((item) => this.mapToContentData(item)); + } + + /** + * Find content by URL + */ + static async findByUrl(url: string): Promise { + const content = await prisma.content.findUnique({ + where: { url }, + include: { + genres: { + include: { + genre: true, + }, + }, + castMembers: { + orderBy: { name: 'asc' }, + }, + }, + }); + + if (!content) { + return null; + } + + return this.mapToContentData(content); + } + + /** + * Create new content from scraper result + */ + static async create( + url: string, + scraperResult: ScraperResult + ): Promise { + // Create or find genres + const genreConnections = await Promise.all( + scraperResult.genres.map(async (genreName) => { + const genre = await prisma.genre.upsert({ + where: { name: genreName }, + update: {}, + create: { name: genreName }, + }); + return { genreId: genre.id }; + }) + ); + + // Create content with genres and cast + const content = await prisma.content.create({ + data: { + url, + title: scraperResult.title, + year: scraperResult.year, + plot: scraperResult.plot, + backdropUrl: scraperResult.backdropUrl, + ageRating: scraperResult.ageRating, + type: scraperResult.type, + currentSeason: scraperResult.currentSeason, + genres: { + create: genreConnections, + }, + castMembers: { + create: scraperResult.cast.map((name) => ({ name })), + }, + }, + include: { + genres: { + include: { + genre: true, + }, + }, + castMembers: { + orderBy: { name: 'asc' }, + }, + }, + }); + + return this.mapToContentData(content); + } + + /** + * Update existing content + */ + static async update( + url: string, + scraperResult: ScraperResult + ): Promise { + // Delete existing genres and cast + const existingContent = await prisma.content.findUnique({ + where: { url }, + }); + + if (existingContent) { + await prisma.contentGenre.deleteMany({ + where: { contentId: existingContent.id }, + }); + await prisma.castMember.deleteMany({ + where: { contentId: existingContent.id }, + }); + } + + // Create or find genres + const genreConnections = await Promise.all( + scraperResult.genres.map(async (genreName) => { + const genre = await prisma.genre.upsert({ + where: { name: genreName }, + update: {}, + create: { name: genreName }, + }); + return { genreId: genre.id }; + }) + ); + + // Update content + const content = await prisma.content.update({ + where: { url }, + data: { + title: scraperResult.title, + year: scraperResult.year, + plot: scraperResult.plot, + backdropUrl: scraperResult.backdropUrl, + ageRating: scraperResult.ageRating, + type: scraperResult.type, + currentSeason: scraperResult.currentSeason, + genres: { + create: genreConnections, + }, + castMembers: { + create: scraperResult.cast.map((name) => ({ name })), + }, + }, + include: { + genres: { + include: { + genre: true, + }, + }, + castMembers: { + orderBy: { name: 'asc' }, + }, + }, + }); + + return this.mapToContentData(content); + } + + /** + * Delete content by URL + */ + static async delete(url: string): Promise { + await prisma.content.delete({ + where: { url }, + }); + } + + /** + * Map database result to ContentData type + */ + private static mapToContentData(content: { + id: string; + url: string; + title: string; + year: number | null; + plot: string | null; + backdropUrl: string | null; + ageRating: string | null; + type: string; + currentSeason: number | null; + createdAt: Date; + updatedAt: Date; + genres: { genre: { name: string } }[]; + castMembers: { name: string }[]; + }): ContentData { + return { + id: content.id, + url: content.url, + title: content.title, + year: content.year, + plot: content.plot, + backdropUrl: content.backdropUrl, + ageRating: content.ageRating, + type: content.type as 'movie' | 'tvshow', + currentSeason: content.currentSeason, + genres: content.genres.map((g) => g.genre.name), + cast: content.castMembers.map((c) => c.name), + createdAt: content.createdAt, + updatedAt: content.updatedAt, + }; + } + + /** + * Convert ContentData to API response format + */ + static toApiResponse(data: ContentData): GetInfoResponse { + return { + title: data.title, + year: data.year, + plot: data.plot, + ageRating: data.ageRating, + type: data.type, + currentSeason: data.currentSeason, + genres: data.genres, + cast: data.cast, + backdrop: data.backdropUrl, + }; + } +} + +export default ContentService; diff --git a/src/services/job.service.ts b/src/services/job.service.ts new file mode 100644 index 0000000..c22f651 --- /dev/null +++ b/src/services/job.service.ts @@ -0,0 +1,237 @@ +import { v4 as uuidv4 } from 'uuid'; +import prisma from '../config/database.js'; +import { CacheService } from './cache.service.js'; +import { ContentService } from './content.service.js'; +import { ScraperService } from './scraper.service.js'; +import { + emitJobProgress, + emitJobCompleted, + emitJobError, +} from '../config/socket.js'; +import logger from '../utils/logger.js'; +import type { + ScrapeJob, + JobStatus, + GetInfoResponse, + DataSource, + ApiError, +} from '../types/index.js'; + +/** + * Job Service for async scrape operations + */ +export class JobService { + /** + * Create a new scrape job + */ + static async create(url: string): Promise { + const job = await prisma.scrapeJob.create({ + data: { + id: uuidv4(), + url, + status: 'pending', + progress: 0, + step: 'created', + }, + }); + + logger.info('Job created', { jobId: job.id, url }); + return this.mapToScrapeJob(job); + } + + /** + * Get job by ID + */ + static async getById(jobId: string): Promise { + const job = await prisma.scrapeJob.findUnique({ + where: { id: jobId }, + }); + + return job ? this.mapToScrapeJob(job) : null; + } + + /** + * Update job status + */ + static async update( + jobId: string, + data: { + status?: JobStatus; + progress?: number; + step?: string; + result?: unknown; + error?: string; + } + ): Promise { + const job = await prisma.scrapeJob.update({ + where: { id: jobId }, + data, + }); + + return this.mapToScrapeJob(job); + } + + /** + * Process a scrape job (hybrid: cache -> db -> netflix) + */ + static async process(jobId: string): Promise { + const job = await this.getById(jobId); + + if (!job) { + logger.error('Job not found', { jobId }); + return; + } + + try { + // Update status to processing + await this.update(jobId, { + status: 'processing', + progress: 10, + step: 'checking_cache', + }); + emitJobProgress(jobId, 10, 'processing', 'Checking cache'); + + // Step 1: Check cache + const cachedData = await CacheService.get(job.url); + if (cachedData) { + await this.completeJob(jobId, cachedData, 'cache'); + return; + } + + // Update progress + await this.update(jobId, { progress: 30, step: 'checking_database' }); + emitJobProgress(jobId, 30, 'processing', 'Checking database'); + + // Step 2: Check database + const dbContent = await ContentService.findByUrl(job.url); + if (dbContent) { + const responseData = ContentService.toApiResponse(dbContent); + + // Cache the result + await CacheService.set(job.url, responseData); + + await this.completeJob(jobId, responseData, 'database'); + return; + } + + // Update progress + await this.update(jobId, { progress: 50, step: 'scraping_netflix' }); + emitJobProgress(jobId, 50, 'processing', 'Scraping Netflix'); + + // Step 3: Scrape from Netflix + const scraperResult = await ScraperService.scrape(job.url); + + // Update progress + await this.update(jobId, { progress: 80, step: 'saving_to_database' }); + emitJobProgress(jobId, 80, 'processing', 'Saving to database'); + + // Step 4: Save to database + const contentData = await ContentService.create(job.url, scraperResult); + const responseData = ContentService.toApiResponse(contentData); + + // Step 5: Cache the result + await CacheService.set(job.url, responseData); + + // Complete the job + await this.completeJob(jobId, responseData, 'netflix'); + } catch (error) { + const apiError: ApiError = { + code: 'SCRAPE_ERROR', + message: error instanceof Error ? error.message : 'Unknown error occurred', + }; + + await this.update(jobId, { + status: 'failed', + error: apiError.message, + }); + + emitJobError(jobId, apiError); + logger.error('Job failed', { + jobId, + error: apiError.message, + }); + } + } + + /** + * Complete a job with result + */ + private static async completeJob( + jobId: string, + data: GetInfoResponse, + source: DataSource + ): Promise { + await this.update(jobId, { + status: 'completed', + progress: 100, + step: 'completed', + result: data, + }); + + emitJobCompleted(jobId, data, source); + logger.info('Job completed', { jobId, source }); + } + + /** + * Process job synchronously (for direct API calls) + */ + static async processSync(url: string): Promise<{ + data: GetInfoResponse; + source: DataSource; + }> { + // Step 1: Check cache + const cachedData = await CacheService.get(url); + if (cachedData) { + return { data: cachedData, source: 'cache' }; + } + + // Step 2: Check database + const dbContent = await ContentService.findByUrl(url); + if (dbContent) { + const responseData = ContentService.toApiResponse(dbContent); + await CacheService.set(url, responseData); + return { data: responseData, source: 'database' }; + } + + // Step 3: Scrape from Netflix + const scraperResult = await ScraperService.scrape(url); + + // Step 4: Save to database + const contentData = await ContentService.create(url, scraperResult); + const responseData = ContentService.toApiResponse(contentData); + + // Step 5: Cache the result + await CacheService.set(url, responseData); + + return { data: responseData, source: 'netflix' }; + } + + /** + * Map database result to ScrapeJob type + */ + private static mapToScrapeJob(job: { + id: string; + url: string; + status: string; + progress: number; + step: string | null; + result: unknown; + error: string | null; + createdAt: Date; + updatedAt: Date; + }): ScrapeJob { + return { + id: job.id, + url: job.url, + status: job.status as JobStatus, + progress: job.progress, + step: job.step || '', + result: job.result as ScrapeJob['result'], + error: job.error ? { code: 'JOB_ERROR', message: job.error } : undefined, + createdAt: job.createdAt, + updatedAt: job.updatedAt, + }; + } +} + +export default JobService; diff --git a/src/services/scraper.service.ts b/src/services/scraper.service.ts new file mode 100644 index 0000000..0a7216d --- /dev/null +++ b/src/services/scraper.service.ts @@ -0,0 +1,284 @@ +import * as cheerio from 'cheerio'; +import type { ScraperResult, ContentType } from '../types/index.js'; +import logger from '../utils/logger.js'; + +/** + * Age rating patterns to detect and exclude from genres + * Supports various formats including Unicode bidirectional text characters + * Unicode chars: \u2066-\u2069 (isolate), \u202A-\u202E (embedding), \u200E-\u200F (marks) + */ +const AGE_RATING_PATTERN = /^[\u2066-\u2069\u202A-\u202E\u200E-\u200F]*(\d+\+|PG-?13|PG|NC-?17|R|G|TV-?MA|TV-?14|TV-?PG|TV-?G|TV-?Y7?-?FV?|TV-?Y)[\u2066-\u2069\u202A-\u202E\u200E-\u200F]*$/i; + +/** + * Season pattern to detect TV shows and extract season number + * Matches patterns like "3 Sezon", "2 Seasons", "1. Sezon", etc. + */ +const SEASON_PATTERN = /(\d+)\.?\s*(sezon|season|sezonlar|seasons)/i; + +/** + * Netflix HTML Scraper Service + * Uses Cheerio for parsing HTML content + */ +export class ScraperService { + /** + * Validate if URL is a valid Netflix URL + */ + static isValidNetflixUrl(url: string): boolean { + try { + const parsedUrl = new URL(url); + const validHosts = [ + 'www.netflix.com', + 'netflix.com', + 'www.netflix.com.tr', + 'netflix.com.tr', + ]; + return validHosts.includes(parsedUrl.hostname); + } catch { + return false; + } + } + + /** + * Extract Netflix title ID from URL + */ + static extractTitleId(url: string): string | null { + const match = url.match(/\/title\/(\d+)/); + return match ? match[1] : null; + } + + /** + * Fetch HTML content from Netflix URL + */ + private static async fetchHtml(url: string): Promise { + logger.info('Fetching Netflix page', { url }); + + const response = await fetch(url, { + headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Accept-Language': 'tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7', + Accept: + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch Netflix page: ${response.status}`); + } + + return response.text(); + } + + /** + * Parse HTML and extract content data + */ + static async scrape(url: string): Promise { + if (!this.isValidNetflixUrl(url)) { + throw new Error('Invalid Netflix URL'); + } + + const html = await this.fetchHtml(url); + const $ = cheerio.load(html); + + const title = this.extractTitle($); + const year = this.extractYear($); + const plot = this.extractPlot($); + const ageRating = this.extractAgeRating($); + const { genres, type, currentSeason } = this.extractGenresTypeAndSeason($); + const cast = this.extractCast($); + const backdropUrl = this.extractBackdrop($); + + const result: ScraperResult = { + title, + year, + plot, + ageRating, + type, + genres, + cast, + backdropUrl, + currentSeason, + }; + + logger.info('Scraping completed', { + url, + title, + year, + ageRating, + type, + genresCount: genres.length, + castCount: cast.length, + }); + + return result; + } + + /** + * Extract title from HTML + */ + private static extractTitle($: cheerio.CheerioAPI): string { + let title = $('h2.default-ltr-iqcdef-cache-tnklrp').first().text().trim(); + + if (!title) { + title = $('meta[property="og:title"]').attr('content') || ''; + } + + if (!title) { + const pageTitle = $('title').text(); + title = pageTitle.replace(' | Netflix', '').trim(); + } + + return title || 'Unknown Title'; + } + + /** + * Extract year from HTML (first li element) + */ + private static extractYear($: cheerio.CheerioAPI): number | null { + const yearText = $('li.default-ltr-iqcdef-cache-6prs41').first().text().trim(); + const year = parseInt(yearText, 10); + + if (!isNaN(year) && year >= 1900 && year <= new Date().getFullYear() + 5) { + return year; + } + + return null; + } + + /** + * Extract plot/description from HTML + */ + private static extractPlot($: cheerio.CheerioAPI): string | null { + const plot = $('span.default-ltr-iqcdef-cache-6ukeej').first().text().trim(); + + if (!plot) { + const metaDesc = $('meta[property="og:description"]').attr('content'); + return metaDesc || null; + } + + return plot || null; + } + + /** + * Extract age rating from HTML (e.g., "18+", "16+") + * Searches all li elements (except first which is year) + */ + private static extractAgeRating($: cheerio.CheerioAPI): string | null { + let ageRating: string | null = null; + const foundTexts: string[] = []; + + $('li.default-ltr-iqcdef-cache-6prs41').each((index, element) => { + if (index === 0) return; // Skip year + + const text = $(element).text().trim(); + foundTexts.push(text); + + // Clean Unicode characters first + const cleanText = text.replace(/[\u2066-\u2069\u202A-\u202E\u200E-\u200F]/g, '').trim(); + + if (cleanText && AGE_RATING_PATTERN.test(cleanText)) { + ageRating = cleanText; + return false; // Break loop + } + }); + + // Debug logging + if (!ageRating && foundTexts.length > 0) { + logger.debug('Age rating not found in elements', { + foundTexts, + pattern: AGE_RATING_PATTERN.source, + }); + } + + return ageRating; + } + + /** + * Extract genres from HTML (skip year, age rating, and season info) + * Also detects content type (movie/tvshow) based on season presence + * Extracts current season number from season text + */ + private static extractGenresTypeAndSeason($: cheerio.CheerioAPI): { genres: string[]; type: ContentType; currentSeason: number | null } { + const genres: string[] = []; + let type: ContentType = 'movie'; + let currentSeason: number | null = null; + const foundTexts: string[] = []; + + $('li.default-ltr-iqcdef-cache-6prs41').each((index, element) => { + if (index === 0) return; // Skip year + + const text = $(element).text().trim(); + const cleanText = text.replace(/[\u2066\u2069\u202A\u202B\u202C\u202D\u202E\u200E\u200F]/g, '').trim(); + foundTexts.push(cleanText); + + // Check for season pattern - indicates TV show + const seasonMatch = cleanText.match(SEASON_PATTERN); + if (cleanText && seasonMatch) { + type = 'tvshow'; + // Extract season number from the text + const seasonNum = parseInt(seasonMatch[1], 10); + if (!isNaN(seasonNum)) { + currentSeason = seasonNum; + } + return; // Skip adding to genres + } + + // Skip age rating - only add actual genres + if (cleanText && !AGE_RATING_PATTERN.test(cleanText)) { + genres.push(cleanText); + } + }); + + // Debug logging + logger.debug('extractGenresTypeAndSeason completed', { + foundTexts, + genres, + type, + currentSeason, + }); + + return { genres, type, currentSeason }; + } + + /** + * Extract cast members from HTML + */ + private static extractCast($: cheerio.CheerioAPI): string[] { + const castText = $('span.default-ltr-iqcdef-cache-m0886o').first().text().trim(); + + if (!castText) { + return []; + } + + return castText + .split(',') + .map((name) => name.trim()) + .filter((name) => name.length > 0); + } + + /** + * Extract backdrop image URL from HTML + */ + private static extractBackdrop($: cheerio.CheerioAPI): string | null { + const backdropDiv = $('div.default-ltr-iqcdef-cache-1wezh7a').first(); + const img = backdropDiv.find('img').first(); + + const srcset = img.attr('srcset'); + if (srcset) { + const sources = srcset.split(','); + const lastSource = sources[sources.length - 1]?.trim().split(' ')[0]; + if (lastSource) { + return lastSource; + } + } + + const src = img.attr('src'); + if (src) { + return src; + } + + return null; + } +} + +export default ScraperService; diff --git a/src/services/tmdb.service.ts b/src/services/tmdb.service.ts new file mode 100644 index 0000000..c375940 --- /dev/null +++ b/src/services/tmdb.service.ts @@ -0,0 +1,429 @@ +import { env } from '../config/env.js'; +import type { + TmdbSearchRequest, + TmdbSearchResult, + TmdbSearchResponse, + TmdbRawResponse, + TmdbRawMovie, + TmdbRawTv, +} from '../types/index.js'; +import logger from '../utils/logger.js'; + +/** + * TMDB Genre ID to Name mapping + * Common genres used in movies and TV shows + */ +const GENRE_MAP: Record = { + 28: 'Action', + 12: 'Adventure', + 16: 'Animation', + 35: 'Comedy', + 80: 'Crime', + 99: 'Documentary', + 18: 'Drama', + 10751: 'Family', + 14: 'Fantasy', + 36: 'History', + 27: 'Horror', + 10402: 'Music', + 9648: 'Mystery', + 10749: 'Romance', + 878: 'Science Fiction', + 10770: 'TV Movie', + 53: 'Thriller', + 10752: 'War', + 37: 'Western', + 10759: 'Action & Adventure', + 10762: 'Kids', + 10763: 'News', + 10764: 'Reality', + 10765: 'Sci-Fi & Fantasy', + 10766: 'Soap', + 10767: 'Talk', + 10768: 'War & Politics', +}; + +/** + * TMDB API Base URL + */ +const TMDB_BASE_URL = 'https://api.themoviedb.org/3'; + +/** + * TMDB Image Base URL + */ +const TMDB_IMAGE_BASE_URL = 'https://image.tmdb.org/t/p/original'; + +/** + * TMDB Service for movie/TV show search + */ +export class TmdbService { + /** + * Get common headers for TMDB API requests + */ + private static getHeaders(): Record { + return { + Authorization: `Bearer ${env.TMDB_ACCESS_TOKEN}`, + 'Content-Type': 'application/json', + }; + } + + /** + * Extract year from date string + */ + private static extractYear(dateStr: string | null): number | null { + if (!dateStr) return null; + const year = parseInt(dateStr.split('-')[0] || '0', 10); + return isNaN(year) ? null : year; + } + + /** + * Convert genre IDs to genre names + */ + private static mapGenreIds(genreIds: number[]): string[] { + return genreIds + .map((id) => GENRE_MAP[id]) + .filter((name): name is string => name !== undefined); + } + + /** + * Build full image URL + */ + private static buildImageUrl(path: string | null): string | null { + if (!path) return null; + return `${TMDB_IMAGE_BASE_URL}${path}`; + } + + /** + * Normalize raw movie result to TmdbSearchResult + */ + private static normalizeMovie(movie: TmdbRawMovie): TmdbSearchResult { + return { + id: movie.id, + title: movie.title, + originalTitle: movie.original_title, + overview: movie.overview, + releaseDate: movie.release_date || null, + year: this.extractYear(movie.release_date), + type: 'movie', + posterPath: this.buildImageUrl(movie.poster_path), + backdropPath: this.buildImageUrl(movie.backdrop_path), + voteAverage: movie.vote_average, + voteCount: movie.vote_count, + popularity: movie.popularity, + genres: this.mapGenreIds(movie.genre_ids), + originalLanguage: movie.original_language, + }; + } + + /** + * Normalize raw TV result to TmdbSearchResult + */ + private static normalizeTv(tv: TmdbRawTv): TmdbSearchResult { + return { + id: tv.id, + title: tv.name, + originalTitle: tv.original_name, + overview: tv.overview, + releaseDate: tv.first_air_date || null, + year: this.extractYear(tv.first_air_date), + type: 'tv', + posterPath: this.buildImageUrl(tv.poster_path), + backdropPath: this.buildImageUrl(tv.backdrop_path), + voteAverage: tv.vote_average, + voteCount: tv.vote_count, + popularity: tv.popularity, + genres: this.mapGenreIds(tv.genre_ids), + originalLanguage: tv.original_language, + currentSeason: null, + totalSeasons: null, + }; + } + + /** + * Get TV show details including season count + */ + private static async getTvDetails(tvId: number): Promise<{ numberOfSeasons: number } | null> { + const url = `${TMDB_BASE_URL}/tv/${tvId}?language=tr-TR`; + + try { + const response = await fetch(url, { + method: 'GET', + headers: this.getHeaders(), + }); + + if (!response.ok) { + return null; + } + + const data = await response.json(); + return { + numberOfSeasons: data.number_of_seasons || 0, + }; + } catch { + return null; + } + } + + /** + * Get specific season details including air date + */ + private static async getSeasonDetails( + tvId: number, + seasonNumber: number + ): Promise<{ airDate: string | null; year: number | null } | null> { + const url = `${TMDB_BASE_URL}/tv/${tvId}/season/${seasonNumber}?language=tr-TR`; + + try { + const response = await fetch(url, { + method: 'GET', + headers: this.getHeaders(), + }); + + if (!response.ok) { + return null; + } + + const data = await response.json(); + const airDate = data.air_date || null; + const year = airDate ? this.extractYear(airDate) : null; + + return { airDate, year }; + } catch { + return null; + } + } + + /** + * Filter and enrich TV results based on season criteria + * Only returns shows that match the season requirements + */ + private static async filterAndEnrichTvResultsBySeason( + results: TmdbSearchResult[], + seasonNumber: number, + seasonYear?: number + ): Promise { + const enrichedResults: TmdbSearchResult[] = []; + + // Process results sequentially to avoid rate limiting + for (const result of results) { + if (result.type !== 'tv') continue; + + // Get TV details + const tvDetails = await this.getTvDetails(result.id); + if (!tvDetails) continue; + + // Check if show has enough seasons + if (tvDetails.numberOfSeasons < seasonNumber) { + logger.debug('TV show filtered out - not enough seasons', { + title: result.title, + totalSeasons: tvDetails.numberOfSeasons, + requestedSeason: seasonNumber, + }); + continue; + } + + // If seasonYear is provided, check if the season's air year matches + if (seasonYear) { + const seasonDetails = await this.getSeasonDetails(result.id, seasonNumber); + if (!seasonDetails || seasonDetails.year !== seasonYear) { + logger.debug('TV show filtered out - season year mismatch', { + title: result.title, + requestedSeason: seasonNumber, + requestedYear: seasonYear, + actualYear: seasonDetails?.year, + }); + continue; + } + } + + // Show matches all criteria - add to results + enrichedResults.push({ + ...result, + totalSeasons: tvDetails.numberOfSeasons, + currentSeason: seasonNumber, + }); + } + + return enrichedResults; + } + + /** + * Normalize raw result based on media type + */ + private static normalizeResult(result: TmdbRawMovie | TmdbRawTv): TmdbSearchResult | null { + const mediaType = result.media_type || ('title' in result ? 'movie' : 'tv'); + + if (mediaType === 'movie') { + return this.normalizeMovie(result as TmdbRawMovie); + } else if (mediaType === 'tv') { + return this.normalizeTv(result as TmdbRawTv); + } + + return null; + } + + /** + * Search for movies + */ + static async searchMovies(query: string, year?: number): Promise { + const params = new URLSearchParams({ + query, + language: 'tr-TR', + }); + + if (year) { + params.append('year', year.toString()); + } + + const url = `${TMDB_BASE_URL}/search/movie?${params.toString()}`; + + logger.info('TMDB: Searching movies', { query, year }); + + const response = await fetch(url, { + method: 'GET', + headers: this.getHeaders(), + }); + + if (!response.ok) { + const errorText = await response.text(); + logger.error('TMDB API error', { status: response.status, error: errorText }); + throw new Error(`TMDB API error: ${response.status}`); + } + + const data: TmdbRawResponse = await response.json(); + + const results = data.results + .map((r) => this.normalizeMovie(r as TmdbRawMovie)) + .filter((r): r is TmdbSearchResult => r !== null); + + return { + page: data.page, + results, + totalPages: data.total_pages, + totalResults: data.total_results, + }; + } + + /** + * Search for TV shows + * @param query Search query + * @param year First air date year (optional - not recommended for accurate results) + * @param seasonNumber Required season number - only shows with this season will be returned + * @param seasonYear Required season year - only shows with matching season air year will be returned + */ + static async searchTv( + query: string, + year?: number, + seasonNumber?: number, + seasonYear?: number + ): Promise { + const params = new URLSearchParams({ + query, + language: 'tr-TR', + }); + + // Note: We don't use year for TV searches when seasonNumber is provided + // because the year from Netflix is the season's year, not the show's first air year + if (year && !seasonNumber) { + params.append('first_air_date_year', year.toString()); + } + + const url = `${TMDB_BASE_URL}/search/tv?${params.toString()}`; + + logger.info('TMDB: Searching TV shows', { query, year, seasonNumber, seasonYear }); + + const response = await fetch(url, { + method: 'GET', + headers: this.getHeaders(), + }); + + if (!response.ok) { + const errorText = await response.text(); + logger.error('TMDB API error', { status: response.status, error: errorText }); + throw new Error(`TMDB API error: ${response.status}`); + } + + const data: TmdbRawResponse = await response.json(); + + let results = data.results + .map((r) => this.normalizeTv(r as TmdbRawTv)) + .filter((r): r is TmdbSearchResult => r !== null); + + // Filter and enrich results based on season criteria + if (seasonNumber !== undefined) { + results = await this.filterAndEnrichTvResultsBySeason(results, seasonNumber, seasonYear); + } + + return { + page: data.page, + results, + totalPages: data.total_pages, + totalResults: results.length, // Update total to reflect filtered count + }; + } + + /** + * Multi search (movies, TV shows, and people) + */ + static async searchMulti(query: string, year?: number): Promise { + const params = new URLSearchParams({ + query, + language: 'tr-TR', + }); + + if (year) { + params.append('year', year.toString()); + } + + const url = `${TMDB_BASE_URL}/search/multi?${params.toString()}`; + + logger.info('TMDB: Multi search', { query, year }); + + const response = await fetch(url, { + method: 'GET', + headers: this.getHeaders(), + }); + + if (!response.ok) { + const errorText = await response.text(); + logger.error('TMDB API error', { status: response.status, error: errorText }); + throw new Error(`TMDB API error: ${response.status}`); + } + + const data: TmdbRawResponse = await response.json(); + + // Filter out person results and normalize + const results = data.results + .filter((r) => r.media_type !== 'person') + .map((r) => this.normalizeResult(r)) + .filter((r): r is TmdbSearchResult => r !== null); + + return { + page: data.page, + results, + totalPages: data.total_pages, + totalResults: data.total_results, + }; + } + + /** + * Search for content based on type + * @param request Search request with query, year, type, and optional season parameters + */ + static async search(request: TmdbSearchRequest): Promise { + const { query, year, type = 'multi', seasonYear, seasonNumber } = request; + + switch (type) { + case 'movie': + return this.searchMovies(query, year); + case 'tv': + // For TV shows, use season parameters if provided + return this.searchTv(query, year, seasonNumber, seasonYear); + case 'multi': + default: + return this.searchMulti(query, year); + } + } +} + +export default TmdbService; diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..fdb40f0 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,210 @@ +/** + * Type definitions for Netflix Scraper API + */ + +// ============================================ +// Content Types +// ============================================ + +export interface ContentData { + id: string; + url: string; + title: string; + year: number | null; + plot: string | null; + backdropUrl: string | null; + ageRating: string | null; + type: 'movie' | 'tvshow'; + currentSeason: number | null; + genres: string[]; + cast: string[]; + createdAt: Date; + updatedAt: Date; +} + +export type ContentType = 'movie' | 'tvshow'; + +export interface ScraperResult { + title: string; + year: number | null; + plot: string | null; + ageRating: string | null; + type: ContentType; + genres: string[]; + cast: string[]; + backdropUrl: string | null; + currentSeason: number | null; +} + +// ============================================ +// API Types +// ============================================ + +export interface ApiResponse { + success: boolean; + data?: T; + error?: ApiError; +} + +export interface ApiError { + code: string; + message: string; + details?: Record; +} + +export interface GetInfoRequest { + url: string; +} + +export interface GetInfoResponse { + title: string; + year: number | null; + plot: string | null; + ageRating: string | null; + type: ContentType; + genres: string[]; + cast: string[]; + backdrop: string | null; + currentSeason: number | null; +} + +// ============================================ +// Cache Types +// ============================================ + +export interface CacheEntry { + data: T; + cachedAt: number; + ttl: number; +} + +export type DataSource = 'cache' | 'database' | 'netflix'; + +// ============================================ +// Socket Event Types +// ============================================ + +export interface SocketEvents { + // Client -> Server + 'job:subscribe': (jobId: string) => void; + 'job:unsubscribe': (jobId: string) => void; + + // Server -> Client + 'job:progress': (data: JobProgress) => void; + 'job:completed': (data: JobCompleted) => void; + 'job:error': (data: JobError) => void; +} + +export interface JobProgress { + jobId: string; + progress: number; // 0-100 + status: string; + step: string; +} + +export interface JobCompleted { + jobId: string; + data: GetInfoResponse; + source: DataSource; +} + +export interface JobError { + jobId: string; + error: ApiError; +} + +// ============================================ +// Job Types +// ============================================ + +export type JobStatus = 'pending' | 'processing' | 'completed' | 'failed'; + +export interface ScrapeJob { + id: string; + url: string; + status: JobStatus; + progress: number; + step: string; + result?: ScraperResult; + error?: ApiError; + createdAt: Date; + updatedAt: Date; +} + +// ============================================ +// TMDB API Types +// ============================================ + +export interface TmdbSearchRequest { + query: string; + year?: number; + type?: 'movie' | 'tv' | 'multi'; + seasonYear?: number; + seasonNumber?: number; +} + +export interface TmdbSearchResult { + id: number; + title: string; + originalTitle: string; + overview: string | null; + releaseDate: string | null; + year: number | null; + type: 'movie' | 'tv'; + posterPath: string | null; + backdropPath: string | null; + voteAverage: number; + voteCount: number; + popularity: number; + genres: string[]; + originalLanguage: string; + currentSeason?: number | null; + totalSeasons?: number | null; +} + +export interface TmdbSearchResponse { + page: number; + results: TmdbSearchResult[]; + totalPages: number; + totalResults: number; +} + +// Raw TMDB API Response Types +export interface TmdbRawMovie { + id: number; + title: string; + original_title: string; + overview: string | null; + release_date: string; + poster_path: string | null; + backdrop_path: string | null; + vote_average: number; + vote_count: number; + popularity: number; + genre_ids: number[]; + original_language: string; + media_type?: 'movie'; +} + +export interface TmdbRawTv { + id: number; + name: string; + original_name: string; + overview: string | null; + first_air_date: string; + poster_path: string | null; + backdrop_path: string | null; + vote_average: number; + vote_count: number; + popularity: number; + genre_ids: number[]; + original_language: string; + media_type?: 'tv'; +} + +export interface TmdbRawResponse { + page: number; + results: (TmdbRawMovie | TmdbRawTv)[]; + total_pages: number; + total_results: number; +} diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..112caab --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,97 @@ +/** + * Structured JSON Logger + * Standardized log levels: debug, info, warn, error + */ + +type LogLevel = 'debug' | 'info' | 'warn' | 'error'; + +interface LogEntry { + timestamp: string; + level: LogLevel; + message: string; + service: string; + traceId?: string; + [key: string]: unknown; +} + +class Logger { + private service: string; + private level: LogLevel; + private levels: Record = { + debug: 0, + info: 1, + warn: 2, + error: 3, + }; + + constructor(service: string = 'netflix-scraper-api') { + this.service = service; + this.level = (process.env.LOG_LEVEL as LogLevel) || 'info'; + } + + private shouldLog(level: LogLevel): boolean { + return this.levels[level] >= this.levels[this.level]; + } + + private formatEntry(level: LogLevel, message: string, data?: Record): LogEntry { + const entry: LogEntry = { + timestamp: new Date().toISOString(), + level, + message, + service: this.service, + }; + + if (data) { + Object.assign(entry, data); + } + + return entry; + } + + private output(entry: LogEntry): void { + const output = JSON.stringify(entry); + if (entry.level === 'error') { + process.stderr.write(output + '\n'); + } else { + process.stdout.write(output + '\n'); + } + } + + debug(message: string, data?: Record): void { + if (this.shouldLog('debug')) { + this.output(this.formatEntry('debug', message, data)); + } + } + + info(message: string, data?: Record): void { + if (this.shouldLog('info')) { + this.output(this.formatEntry('info', message, data)); + } + } + + warn(message: string, data?: Record): void { + if (this.shouldLog('warn')) { + this.output(this.formatEntry('warn', message, data)); + } + } + + error(message: string, data?: Record): void { + if (this.shouldLog('error')) { + this.output(this.formatEntry('error', message, data)); + } + } + + withContext(context: Record): Logger { + const childLogger = new Logger(this.service); + const parentLog = this.formatEntry.bind(this); + + childLogger.formatEntry = (level: LogLevel, message: string, data?: Record) => { + return parentLog(level, message, { ...context, ...data }); + }; + + return childLogger; + } +} + +export const logger = new Logger(); +export default logger; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d424bf4 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "incremental": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "exactOptionalPropertyTypes": false, + "noUncheckedIndexedAccess": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo new file mode 100644 index 0000000..3e97931 --- /dev/null +++ b/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"fileNames":["./node_modules/typescript/lib/lib.es5.d.ts","./node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/typescript/lib/lib.es2022.object.d.ts","./node_modules/typescript/lib/lib.es2022.string.d.ts","./node_modules/typescript/lib/lib.es2022.regexp.d.ts","./node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/@types/node/compatibility/disposable.d.ts","./node_modules/@types/node/compatibility/indexable.d.ts","./node_modules/@types/node/compatibility/iterators.d.ts","./node_modules/@types/node/compatibility/index.d.ts","./node_modules/@types/node/globals.typedarray.d.ts","./node_modules/@types/node/buffer.buffer.d.ts","./node_modules/@types/node/globals.d.ts","./node_modules/@types/node/web-globals/abortcontroller.d.ts","./node_modules/@types/node/web-globals/domexception.d.ts","./node_modules/@types/node/web-globals/events.d.ts","./node_modules/undici-types/header.d.ts","./node_modules/undici-types/readable.d.ts","./node_modules/undici-types/file.d.ts","./node_modules/undici-types/fetch.d.ts","./node_modules/undici-types/formdata.d.ts","./node_modules/undici-types/connector.d.ts","./node_modules/undici-types/client.d.ts","./node_modules/undici-types/errors.d.ts","./node_modules/undici-types/dispatcher.d.ts","./node_modules/undici-types/global-dispatcher.d.ts","./node_modules/undici-types/global-origin.d.ts","./node_modules/undici-types/pool-stats.d.ts","./node_modules/undici-types/pool.d.ts","./node_modules/undici-types/handlers.d.ts","./node_modules/undici-types/balanced-pool.d.ts","./node_modules/undici-types/agent.d.ts","./node_modules/undici-types/mock-interceptor.d.ts","./node_modules/undici-types/mock-agent.d.ts","./node_modules/undici-types/mock-client.d.ts","./node_modules/undici-types/mock-pool.d.ts","./node_modules/undici-types/mock-errors.d.ts","./node_modules/undici-types/proxy-agent.d.ts","./node_modules/undici-types/env-http-proxy-agent.d.ts","./node_modules/undici-types/retry-handler.d.ts","./node_modules/undici-types/retry-agent.d.ts","./node_modules/undici-types/api.d.ts","./node_modules/undici-types/interceptors.d.ts","./node_modules/undici-types/util.d.ts","./node_modules/undici-types/cookies.d.ts","./node_modules/undici-types/patch.d.ts","./node_modules/undici-types/websocket.d.ts","./node_modules/undici-types/eventsource.d.ts","./node_modules/undici-types/filereader.d.ts","./node_modules/undici-types/diagnostics-channel.d.ts","./node_modules/undici-types/content-type.d.ts","./node_modules/undici-types/cache.d.ts","./node_modules/undici-types/index.d.ts","./node_modules/@types/node/web-globals/fetch.d.ts","./node_modules/@types/node/web-globals/navigator.d.ts","./node_modules/@types/node/web-globals/storage.d.ts","./node_modules/@types/node/assert.d.ts","./node_modules/@types/node/assert/strict.d.ts","./node_modules/@types/node/async_hooks.d.ts","./node_modules/@types/node/buffer.d.ts","./node_modules/@types/node/child_process.d.ts","./node_modules/@types/node/cluster.d.ts","./node_modules/@types/node/console.d.ts","./node_modules/@types/node/constants.d.ts","./node_modules/@types/node/crypto.d.ts","./node_modules/@types/node/dgram.d.ts","./node_modules/@types/node/diagnostics_channel.d.ts","./node_modules/@types/node/dns.d.ts","./node_modules/@types/node/dns/promises.d.ts","./node_modules/@types/node/domain.d.ts","./node_modules/@types/node/events.d.ts","./node_modules/@types/node/fs.d.ts","./node_modules/@types/node/fs/promises.d.ts","./node_modules/@types/node/http.d.ts","./node_modules/@types/node/http2.d.ts","./node_modules/@types/node/https.d.ts","./node_modules/@types/node/inspector.d.ts","./node_modules/@types/node/inspector.generated.d.ts","./node_modules/@types/node/module.d.ts","./node_modules/@types/node/net.d.ts","./node_modules/@types/node/os.d.ts","./node_modules/@types/node/path.d.ts","./node_modules/@types/node/perf_hooks.d.ts","./node_modules/@types/node/process.d.ts","./node_modules/@types/node/punycode.d.ts","./node_modules/@types/node/querystring.d.ts","./node_modules/@types/node/readline.d.ts","./node_modules/@types/node/readline/promises.d.ts","./node_modules/@types/node/repl.d.ts","./node_modules/@types/node/sea.d.ts","./node_modules/@types/node/sqlite.d.ts","./node_modules/@types/node/stream.d.ts","./node_modules/@types/node/stream/promises.d.ts","./node_modules/@types/node/stream/consumers.d.ts","./node_modules/@types/node/stream/web.d.ts","./node_modules/@types/node/string_decoder.d.ts","./node_modules/@types/node/test.d.ts","./node_modules/@types/node/timers.d.ts","./node_modules/@types/node/timers/promises.d.ts","./node_modules/@types/node/tls.d.ts","./node_modules/@types/node/trace_events.d.ts","./node_modules/@types/node/tty.d.ts","./node_modules/@types/node/url.d.ts","./node_modules/@types/node/util.d.ts","./node_modules/@types/node/v8.d.ts","./node_modules/@types/node/vm.d.ts","./node_modules/@types/node/wasi.d.ts","./node_modules/@types/node/worker_threads.d.ts","./node_modules/@types/node/zlib.d.ts","./node_modules/@types/node/index.d.ts","./node_modules/@types/send/index.d.ts","./node_modules/@types/qs/index.d.ts","./node_modules/@types/range-parser/index.d.ts","./node_modules/@types/express-serve-static-core/index.d.ts","./node_modules/@types/http-errors/index.d.ts","./node_modules/@types/mime/index.d.ts","./node_modules/@types/serve-static/node_modules/@types/send/index.d.ts","./node_modules/@types/serve-static/index.d.ts","./node_modules/@types/connect/index.d.ts","./node_modules/@types/body-parser/index.d.ts","./node_modules/@types/express/index.d.ts","./node_modules/zod/v3/helpers/typealiases.d.cts","./node_modules/zod/v3/helpers/util.d.cts","./node_modules/zod/v3/index.d.cts","./node_modules/zod/v3/zoderror.d.cts","./node_modules/zod/v3/locales/en.d.cts","./node_modules/zod/v3/errors.d.cts","./node_modules/zod/v3/helpers/parseutil.d.cts","./node_modules/zod/v3/helpers/enumutil.d.cts","./node_modules/zod/v3/helpers/errorutil.d.cts","./node_modules/zod/v3/helpers/partialutil.d.cts","./node_modules/zod/v3/standard-schema.d.cts","./node_modules/zod/v3/types.d.cts","./node_modules/zod/v3/external.d.cts","./node_modules/zod/index.d.cts","./src/config/env.ts","./node_modules/@prisma/client/runtime/library.d.ts","./node_modules/.prisma/client/index.d.ts","./node_modules/.prisma/client/default.d.ts","./node_modules/@prisma/client/default.d.ts","./src/utils/logger.ts","./src/config/database.ts","./node_modules/ioredis/built/types.d.ts","./node_modules/ioredis/built/command.d.ts","./node_modules/ioredis/built/scanstream.d.ts","./node_modules/ioredis/built/utils/rediscommander.d.ts","./node_modules/ioredis/built/transaction.d.ts","./node_modules/ioredis/built/utils/commander.d.ts","./node_modules/ioredis/built/connectors/abstractconnector.d.ts","./node_modules/ioredis/built/connectors/connectorconstructor.d.ts","./node_modules/ioredis/built/connectors/sentinelconnector/types.d.ts","./node_modules/ioredis/built/connectors/sentinelconnector/sentineliterator.d.ts","./node_modules/ioredis/built/connectors/sentinelconnector/index.d.ts","./node_modules/ioredis/built/connectors/standaloneconnector.d.ts","./node_modules/ioredis/built/redis/redisoptions.d.ts","./node_modules/ioredis/built/cluster/util.d.ts","./node_modules/ioredis/built/cluster/clusteroptions.d.ts","./node_modules/ioredis/built/cluster/index.d.ts","./node_modules/denque/index.d.ts","./node_modules/ioredis/built/subscriptionset.d.ts","./node_modules/ioredis/built/datahandler.d.ts","./node_modules/ioredis/built/redis.d.ts","./node_modules/ioredis/built/pipeline.d.ts","./node_modules/ioredis/built/index.d.ts","./src/config/redis.ts","./node_modules/engine.io-parser/build/cjs/commons.d.ts","./node_modules/engine.io-parser/build/cjs/encodepacket.d.ts","./node_modules/engine.io-parser/build/cjs/decodepacket.d.ts","./node_modules/engine.io-parser/build/cjs/index.d.ts","./node_modules/engine.io/build/parser-v3/index.d.ts","./node_modules/engine.io/build/transport.d.ts","./node_modules/engine.io/build/socket.d.ts","./node_modules/@types/cors/index.d.ts","./node_modules/engine.io/build/contrib/types.cookie.d.ts","./node_modules/engine.io/build/server.d.ts","./node_modules/engine.io/build/transports/polling.d.ts","./node_modules/engine.io/build/transports/websocket.d.ts","./node_modules/engine.io/build/transports/webtransport.d.ts","./node_modules/engine.io/build/transports/index.d.ts","./node_modules/engine.io/build/userver.d.ts","./node_modules/engine.io/build/engine.io.d.ts","./node_modules/@socket.io/component-emitter/lib/cjs/index.d.ts","./node_modules/socket.io-parser/build/cjs/index.d.ts","./node_modules/socket.io/dist/typed-events.d.ts","./node_modules/socket.io/dist/client.d.ts","./node_modules/socket.io-adapter/dist/in-memory-adapter.d.ts","./node_modules/socket.io-adapter/dist/cluster-adapter.d.ts","./node_modules/socket.io-adapter/dist/index.d.ts","./node_modules/socket.io/dist/socket-types.d.ts","./node_modules/socket.io/dist/broadcast-operator.d.ts","./node_modules/socket.io/dist/socket.d.ts","./node_modules/socket.io/dist/namespace.d.ts","./node_modules/socket.io/dist/index.d.ts","./src/config/socket.ts","./node_modules/express-rate-limit/dist/index.d.cts","./src/types/index.ts","./src/middleware/ratelimit.middleware.ts","./src/middleware/error.middleware.ts","./src/middleware/auth.middleware.ts","./src/middleware/validation.middleware.ts","./node_modules/@types/uuid/index.d.ts","./src/services/cache.service.ts","./src/services/content.service.ts","./node_modules/domelementtype/lib/index.d.ts","./node_modules/domhandler/lib/node.d.ts","./node_modules/domhandler/lib/index.d.ts","./node_modules/htmlparser2/dist/commonjs/tokenizer.d.ts","./node_modules/htmlparser2/dist/commonjs/parser.d.ts","./node_modules/dom-serializer/lib/index.d.ts","./node_modules/domutils/lib/stringify.d.ts","./node_modules/domutils/lib/traversal.d.ts","./node_modules/domutils/lib/manipulation.d.ts","./node_modules/domutils/lib/querying.d.ts","./node_modules/domutils/lib/legacy.d.ts","./node_modules/domutils/lib/helpers.d.ts","./node_modules/domutils/lib/feeds.d.ts","./node_modules/domutils/lib/index.d.ts","./node_modules/htmlparser2/dist/commonjs/index.d.ts","./node_modules/parse5/dist/cjs/common/html.d.ts","./node_modules/parse5/dist/cjs/common/token.d.ts","./node_modules/parse5/dist/cjs/common/error-codes.d.ts","./node_modules/parse5/dist/cjs/tokenizer/preprocessor.d.ts","./node_modules/parse5/node_modules/entities/dist/commonjs/generated/decode-data-html.d.ts","./node_modules/parse5/node_modules/entities/dist/commonjs/generated/decode-data-xml.d.ts","./node_modules/parse5/node_modules/entities/dist/commonjs/decode-codepoint.d.ts","./node_modules/parse5/node_modules/entities/dist/commonjs/decode.d.ts","./node_modules/parse5/dist/cjs/tokenizer/index.d.ts","./node_modules/parse5/dist/cjs/tree-adapters/interface.d.ts","./node_modules/parse5/dist/cjs/parser/open-element-stack.d.ts","./node_modules/parse5/dist/cjs/parser/formatting-element-list.d.ts","./node_modules/parse5/dist/cjs/parser/index.d.ts","./node_modules/parse5/dist/cjs/tree-adapters/default.d.ts","./node_modules/parse5/dist/cjs/serializer/index.d.ts","./node_modules/parse5/dist/cjs/common/foreign-content.d.ts","./node_modules/parse5/dist/cjs/index.d.ts","./node_modules/parse5-htmlparser2-tree-adapter/dist/cjs/index.d.ts","./node_modules/css-what/lib/es/types.d.ts","./node_modules/css-what/lib/es/parse.d.ts","./node_modules/css-what/lib/es/stringify.d.ts","./node_modules/css-what/lib/es/index.d.ts","./node_modules/css-select/lib/types.d.ts","./node_modules/css-select/lib/pseudo-selectors/filters.d.ts","./node_modules/css-select/lib/pseudo-selectors/pseudos.d.ts","./node_modules/css-select/lib/pseudo-selectors/aliases.d.ts","./node_modules/css-select/lib/pseudo-selectors/index.d.ts","./node_modules/css-select/lib/index.d.ts","./node_modules/cheerio-select/lib/index.d.ts","./node_modules/cheerio/dist/commonjs/options.d.ts","./node_modules/cheerio/dist/commonjs/api/attributes.d.ts","./node_modules/cheerio/dist/commonjs/api/traversing.d.ts","./node_modules/cheerio/dist/commonjs/api/manipulation.d.ts","./node_modules/cheerio/dist/commonjs/api/css.d.ts","./node_modules/cheerio/dist/commonjs/api/forms.d.ts","./node_modules/cheerio/dist/commonjs/api/extract.d.ts","./node_modules/cheerio/dist/commonjs/cheerio.d.ts","./node_modules/cheerio/dist/commonjs/types.d.ts","./node_modules/cheerio/dist/commonjs/static.d.ts","./node_modules/cheerio/dist/commonjs/load.d.ts","./node_modules/cheerio/dist/commonjs/load-parse.d.ts","./node_modules/cheerio/dist/commonjs/slim.d.ts","./node_modules/encoding-sniffer/dist/commonjs/sniffer.d.ts","./node_modules/encoding-sniffer/dist/commonjs/index.d.ts","./node_modules/undici/types/utility.d.ts","./node_modules/undici/types/header.d.ts","./node_modules/undici/types/readable.d.ts","./node_modules/undici/types/fetch.d.ts","./node_modules/undici/types/formdata.d.ts","./node_modules/undici/types/connector.d.ts","./node_modules/undici/types/client-stats.d.ts","./node_modules/undici/types/client.d.ts","./node_modules/undici/types/errors.d.ts","./node_modules/undici/types/dispatcher.d.ts","./node_modules/undici/types/global-dispatcher.d.ts","./node_modules/undici/types/global-origin.d.ts","./node_modules/undici/types/pool-stats.d.ts","./node_modules/undici/types/pool.d.ts","./node_modules/undici/types/handlers.d.ts","./node_modules/undici/types/balanced-pool.d.ts","./node_modules/undici/types/round-robin-pool.d.ts","./node_modules/undici/types/h2c-client.d.ts","./node_modules/undici/types/agent.d.ts","./node_modules/undici/types/mock-interceptor.d.ts","./node_modules/undici/types/mock-call-history.d.ts","./node_modules/undici/types/mock-agent.d.ts","./node_modules/undici/types/mock-client.d.ts","./node_modules/undici/types/mock-pool.d.ts","./node_modules/undici/types/snapshot-agent.d.ts","./node_modules/undici/types/mock-errors.d.ts","./node_modules/undici/types/proxy-agent.d.ts","./node_modules/undici/types/env-http-proxy-agent.d.ts","./node_modules/undici/types/retry-handler.d.ts","./node_modules/undici/types/retry-agent.d.ts","./node_modules/undici/types/api.d.ts","./node_modules/undici/types/cache-interceptor.d.ts","./node_modules/undici/types/interceptors.d.ts","./node_modules/undici/types/util.d.ts","./node_modules/undici/types/cookies.d.ts","./node_modules/undici/types/patch.d.ts","./node_modules/undici/types/websocket.d.ts","./node_modules/undici/types/eventsource.d.ts","./node_modules/undici/types/diagnostics-channel.d.ts","./node_modules/undici/types/content-type.d.ts","./node_modules/undici/types/cache.d.ts","./node_modules/undici/types/index.d.ts","./node_modules/undici/index.d.ts","./node_modules/cheerio/dist/commonjs/index.d.ts","./src/services/scraper.service.ts","./src/services/job.service.ts","./src/routes/api.routes.ts","./src/services/tmdb.service.ts","./src/routes/tmdb.routes.ts","./src/routes/health.routes.ts","./src/index.ts"],"fileIdsList":[[63,111,128,129,189],[63,111,128,129,188],[63,111,128,129,190],[63,111,128,129],[63,111,125,128,129,161,170],[63,111,125,128,129,161],[63,111,122,125,128,129,161,162,163,164],[63,111,128,129,163,165,169,171],[63,108,109,111,128,129],[63,110,111,128,129],[111,128,129],[63,111,116,128,129,146],[63,111,112,117,122,128,129,131,143,154],[63,111,112,113,122,128,129,131],[58,59,60,63,111,128,129],[63,111,114,128,129,155],[63,111,115,116,123,128,129,132],[63,111,116,128,129,143,151],[63,111,117,119,122,128,129,131],[63,110,111,118,128,129],[63,111,119,120,128,129],[63,111,121,122,128,129],[63,110,111,122,128,129],[63,111,122,123,124,128,129,143,154],[63,111,122,123,124,128,129,138,143,146],[63,104,111,119,122,125,128,129,131,143,154],[63,111,122,123,125,126,128,129,131,143,151,154],[63,111,125,127,128,129,143,151,154],[61,62,63,64,65,66,67,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160],[63,111,122,128,129],[63,111,128,129,130,154],[63,111,119,122,128,129,131,143],[63,111,128,129,132],[63,111,128,129,133],[63,110,111,128,129,134],[63,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160],[63,111,128,129,136],[63,111,128,129,137],[63,111,122,128,129,138,139],[63,111,128,129,138,140,155,157],[63,111,123,128,129],[63,111,122,128,129,143,144,146],[63,111,128,129,145,146],[63,111,128,129,143,144],[63,111,128,129,146],[63,111,128,129,147],[63,108,111,128,129,143,148,154],[63,111,122,128,129,149,150],[63,111,128,129,149,150],[63,111,116,128,129,131,143,151],[63,111,128,129,152],[63,111,128,129,131,153],[63,111,125,128,129,137,154],[63,111,116,128,129,155],[63,111,128,129,143,156],[63,111,128,129,130,157],[63,111,128,129,158],[63,104,111,128,129],[63,104,111,122,124,128,129,134,143,146,154,156,157,159],[63,111,128,129,143,160],[63,111,123,128,129,143,161],[63,111,125,128,129,161,166,168],[63,111,123,128,129,143,161,167],[63,111,128,129,257,297],[63,111,128,129,257,306],[63,111,128,129,257,300,306],[63,111,128,129,257,306,307],[63,111,128,129,257,299,300,301,302,303,304,305,307],[63,111,128,129,143,299,307,308,309,310,311,313,356],[63,111,128,129,257,299,309],[63,111,128,129,257,299,306,307,308],[63,111,128,129,257,260,269,286,287,298],[63,111,128,129,257,299,306,307,308,309],[63,111,128,129,257,299,305,306,307,309],[63,111,128,129,291,292,296],[63,111,128,129,292],[63,111,128,129,291,292,293,294,295],[63,111,128,129,291,292],[63,111,128,129,291],[63,111,128,129,288,289,290],[63,111,128,129,288],[63,111,128,129,257],[63,111,128,129,256],[63,111,128,129,255],[63,111,128,129,257,261,262,263,264,265,266,267],[63,111,128,129,255,257],[63,111,128,129,257,260],[63,111,128,129,143,312],[63,111,128,129,217],[63,111,128,129,217,218,219],[63,111,125,128,129,220,222,223,226,230,231],[63,111,122,125,128,129,143,222,223,224,225],[63,111,122,125,128,129,220,222,226],[63,111,122,125,128,129,220,221],[63,111,128,129,222,227,228,229],[63,111,128,129,220,222],[63,111,128,129,222],[63,111,128,129,222,226],[63,111,128,129,172],[63,111,128,129,255,257,258,259,268],[63,111,128,129,258],[63,111,119,128,129,161,199,206,207],[63,111,122,128,129,161,194,195,196,198,199,207,208,213],[63,111,119,128,129,161],[63,111,128,129,161,194],[63,111,128,129,194],[63,111,128,129,200],[63,111,122,128,129,151,161,194,200,202,203,208],[63,111,128,129,202],[63,111,128,129,206],[63,111,128,129,131,151,161,194,200],[63,111,122,128,129,161,194,210,211],[63,111,128,129,194,195,196,197,200,204,205,206,207,208,209,213,214],[63,111,128,129,195,199,209,213],[63,111,122,128,129,161,194,195,196,198,199,206,209,210,212],[63,111,128,129,199,201,204,205],[63,111,128,129,143,161],[63,111,128,129,195],[63,111,128,129,197],[63,111,128,129,131,151,161],[63,111,128,129,194,195,197],[63,111,128,129,257,286],[63,111,128,129,271],[63,111,128,129,270,271],[63,111,128,129,270],[63,111,128,129,270,271,272,278,279,282,283,284,285],[63,111,128,129,271,279],[63,111,128,129,270,271,272,278,279,280,281],[63,111,128,129,270,279],[63,111,128,129,279,283],[63,111,128,129,271,272,273,277],[63,111,128,129,272],[63,111,128,129,270,271,279],[63,111,128,129,274,275,276],[63,111,128,129,237],[63,111,128,129,237,238],[63,111,128,129,233],[63,111,128,129,235,239,240],[63,111,125,128,129,232,234,235,242,244],[63,111,125,126,127,128,129,232,234,235,239,240,241,242,243],[63,111,128,129,235,236,239,241,242,244],[63,111,125,128,129,137],[63,111,125,128,129,232,234,235,236,239,240,241,243],[63,76,80,111,128,129,154],[63,76,111,128,129,143,154],[63,71,111,128,129],[63,73,76,111,128,129,151,154],[63,111,128,129,131,151],[63,111,128,129,161],[63,71,111,128,129,161],[63,73,76,111,128,129,131,154],[63,68,69,72,75,111,122,128,129,143,154],[63,76,83,111,128,129],[63,68,74,111,128,129],[63,76,97,98,111,128,129],[63,72,76,111,128,129,146,154,161],[63,97,111,128,129,161],[63,70,71,111,128,129,161],[63,76,111,128,129],[63,70,71,72,73,74,75,76,77,78,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,98,99,100,101,102,103,111,128,129],[63,76,91,111,128,129],[63,76,83,84,111,128,129],[63,74,76,84,85,111,128,129],[63,75,111,128,129],[63,68,71,76,111,128,129],[63,76,80,84,85,111,128,129],[63,80,111,128,129],[63,74,76,79,111,128,129,154],[63,68,73,76,83,111,128,129],[63,111,128,129,143],[63,71,76,97,111,128,129,159,161],[63,111,128,129,355],[63,111,128,129,154,320,323,326,327],[63,111,128,129,143,154,323],[63,111,128,129,154,323,327],[63,111,128,129,317],[63,111,128,129,321],[63,111,128,129,154,319,320,323],[63,111,128,129,161,317],[63,111,128,129,131,154,319,323],[63,111,122,128,129,143,154,314,315,316,318,322],[63,111,128,129,323,332,340],[63,111,128,129,315,321],[63,111,128,129,323,349,350],[63,111,128,129,146,154,161,315,318,323],[63,111,128,129,323],[63,111,128,129,154,319,323],[63,111,128,129,314],[63,111,128,129,317,318,319,321,322,323,324,325,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,350,351,352,353,354],[63,111,119,128,129,323,342,345],[63,111,128,129,323,332,333,334],[63,111,128,129,321,323,333,335],[63,111,128,129,322],[63,111,128,129,315,317,323],[63,111,128,129,323,327,333,335],[63,111,128,129,327],[63,111,128,129,154,321,323,326],[63,111,128,129,315,319,323,332],[63,111,128,129,323,342],[63,111,128,129,335],[63,111,128,129,146,159,161,317,323,349],[63,111,128,129,185],[63,111,128,129,176,177],[63,111,128,129,173,174,176,178,179,184],[63,111,128,129,174,176],[63,111,128,129,184],[63,111,128,129,176],[63,111,128,129,173,174,176,179,180,181,182,183],[63,111,128,129,173,174,175],[63,111,128,129,187,191,192],[63,111,128,129,186],[63,111,128,129,187,192,215],[63,111,125,128,129,192,244],[63,111,125,128,129,172,187,192,193,216,245,248,249,360,362,363],[63,111,128,129,172,187,192,247],[63,111,128,129,172,192,247],[63,111,128,129,187,192,246,247],[63,111,128,129,172,186,247],[63,111,128,129,172,186,247,248,250,251,254,359],[63,111,128,129,172,187,193,216],[63,111,128,129,172,186,247,248,250,361],[63,111,128,129,187,192,216,247],[63,111,128,129,193,247],[63,111,128,129,192,193,245,247,252,253,254,358],[63,111,128,129,192,247,357],[63,111,128,129,187,192,247]],"fileInfos":[{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","affectsGlobalScope":true,"impliedFormat":1},{"version":"8cdf8847677ac7d20486e54dd3fcf09eda95812ac8ace44b4418da1bbbab6eb8","affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"6c7176368037af28cb72f2392010fa1cef295d6d6744bca8cfb54985f3a18c3e","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab41ef1f2cdafb8df48be20cd969d875602483859dc194e9c97c8a576892c052","affectsGlobalScope":true,"impliedFormat":1},{"version":"437e20f2ba32abaeb7985e0afe0002de1917bc74e949ba585e49feba65da6ca1","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"98cffbf06d6bab333473c70a893770dbe990783904002c4f1a960447b4b53dca","affectsGlobalScope":true,"impliedFormat":1},{"version":"3af97acf03cc97de58a3a4bc91f8f616408099bc4233f6d0852e72a8ffb91ac9","affectsGlobalScope":true,"impliedFormat":1},{"version":"808069bba06b6768b62fd22429b53362e7af342da4a236ed2d2e1c89fcca3b4a","affectsGlobalScope":true,"impliedFormat":1},{"version":"1db0b7dca579049ca4193d034d835f6bfe73096c73663e5ef9a0b5779939f3d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"f26b11d8d8e4b8028f1c7d618b22274c892e4b0ef5b3678a8ccbad85419aef43","affectsGlobalScope":true,"impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"b52476feb4a0cbcb25e5931b930fc73cb6643fb1a5060bf8a3dda0eeae5b4b68","affectsGlobalScope":true,"impliedFormat":1},{"version":"f9501cc13ce624c72b61f12b3963e84fad210fbdf0ffbc4590e08460a3f04eba","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0fa06ada475b910e2106c98c68b10483dc8811d0c14a8a8dd36efb2672485b29","impliedFormat":1},{"version":"33e5e9aba62c3193d10d1d33ae1fa75c46a1171cf76fef750777377d53b0303f","impliedFormat":1},{"version":"2b06b93fd01bcd49d1a6bd1f9b65ddcae6480b9a86e9061634d6f8e354c1468f","impliedFormat":1},{"version":"6a0cd27e5dc2cfbe039e731cf879d12b0e2dded06d1b1dedad07f7712de0d7f4","affectsGlobalScope":true,"impliedFormat":1},{"version":"13f5c844119c43e51ce777c509267f14d6aaf31eafb2c2b002ca35584cd13b29","impliedFormat":1},{"version":"e60477649d6ad21542bd2dc7e3d9ff6853d0797ba9f689ba2f6653818999c264","impliedFormat":1},{"version":"c2510f124c0293ab80b1777c44d80f812b75612f297b9857406468c0f4dafe29","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"4c829ab315f57c5442c6667b53769975acbf92003a66aef19bce151987675bd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"b2ade7657e2db96d18315694789eff2ddd3d8aea7215b181f8a0b303277cc579","impliedFormat":1},{"version":"9855e02d837744303391e5623a531734443a5f8e6e8755e018c41d63ad797db2","impliedFormat":1},{"version":"4d631b81fa2f07a0e63a9a143d6a82c25c5f051298651a9b69176ba28930756d","impliedFormat":1},{"version":"836a356aae992ff3c28a0212e3eabcb76dd4b0cc06bcb9607aeef560661b860d","impliedFormat":1},{"version":"1e0d1f8b0adfa0b0330e028c7941b5a98c08b600efe7f14d2d2a00854fb2f393","impliedFormat":1},{"version":"41670ee38943d9cbb4924e436f56fc19ee94232bc96108562de1a734af20dc2c","affectsGlobalScope":true,"impliedFormat":1},{"version":"c906fb15bd2aabc9ed1e3f44eb6a8661199d6c320b3aa196b826121552cb3695","impliedFormat":1},{"version":"22295e8103f1d6d8ea4b5d6211e43421fe4564e34d0dd8e09e520e452d89e659","impliedFormat":1},{"version":"f949f7f6c7802a338039cfc2156d1fe285cdd1e092c64437ebe15ae8edc854e0","impliedFormat":1},{"version":"6b4e081d55ac24fc8a4631d5dd77fe249fa25900abd7d046abb87d90e3b45645","impliedFormat":1},{"version":"a10f0e1854f3316d7ee437b79649e5a6ae3ae14ffe6322b02d4987071a95362e","impliedFormat":1},{"version":"e208f73ef6a980104304b0d2ca5f6bf1b85de6009d2c7e404028b875020fa8f2","impliedFormat":1},{"version":"d163b6bc2372b4f07260747cbc6c0a6405ab3fbcea3852305e98ac43ca59f5bc","impliedFormat":1},{"version":"e6fa9ad47c5f71ff733744a029d1dc472c618de53804eae08ffc243b936f87ff","affectsGlobalScope":true,"impliedFormat":1},{"version":"83e63d6ccf8ec004a3bb6d58b9bb0104f60e002754b1e968024b320730cc5311","impliedFormat":1},{"version":"24826ed94a78d5c64bd857570fdbd96229ad41b5cb654c08d75a9845e3ab7dde","impliedFormat":1},{"version":"8b479a130ccb62e98f11f136d3ac80f2984fdc07616516d29881f3061f2dd472","impliedFormat":1},{"version":"928af3d90454bf656a52a48679f199f64c1435247d6189d1caf4c68f2eaf921f","affectsGlobalScope":true,"impliedFormat":1},{"version":"bceb58df66ab8fb00170df20cd813978c5ab84be1d285710c4eb005d8e9d8efb","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f16a7e4deafa527ed9995a772bb380eb7d3c2c0fd4ae178c5263ed18394db2c","impliedFormat":1},{"version":"933921f0bb0ec12ef45d1062a1fc0f27635318f4d294e4d99de9a5493e618ca2","impliedFormat":1},{"version":"71a0f3ad612c123b57239a7749770017ecfe6b66411488000aba83e4546fde25","impliedFormat":1},{"version":"77fbe5eecb6fac4b6242bbf6eebfc43e98ce5ccba8fa44e0ef6a95c945ff4d98","impliedFormat":1},{"version":"4f9d8ca0c417b67b69eeb54c7ca1bedd7b56034bb9bfd27c5d4f3bc4692daca7","impliedFormat":1},{"version":"814118df420c4e38fe5ae1b9a3bafb6e9c2aa40838e528cde908381867be6466","impliedFormat":1},{"version":"a3fc63c0d7b031693f665f5494412ba4b551fe644ededccc0ab5922401079c95","impliedFormat":1},{"version":"f27524f4bef4b6519c604bdb23bf4465bddcccbf3f003abb901acbd0d7404d99","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"45650f47bfb376c8a8ed39d4bcda5902ab899a3150029684ee4c10676d9fbaee","impliedFormat":1},{"version":"6b039f55681caaf111d5eb84d292b9bee9e0131d0db1ad0871eef0964f533c73","affectsGlobalScope":true,"impliedFormat":1},{"version":"18fd40412d102c5564136f29735e5d1c3b455b8a37f920da79561f1fde068208","impliedFormat":1},{"version":"c8d3e5a18ba35629954e48c4cc8f11dc88224650067a172685c736b27a34a4dc","impliedFormat":1},{"version":"f0be1b8078cd549d91f37c30c222c2a187ac1cf981d994fb476a1adc61387b14","affectsGlobalScope":true,"impliedFormat":1},{"version":"0aaed1d72199b01234152f7a60046bc947f1f37d78d182e9ae09c4289e06a592","impliedFormat":1},{"version":"2b55d426ff2b9087485e52ac4bc7cfafe1dc420fc76dad926cd46526567c501a","impliedFormat":1},{"version":"66ba1b2c3e3a3644a1011cd530fb444a96b1b2dfe2f5e837a002d41a1a799e60","impliedFormat":1},{"version":"7e514f5b852fdbc166b539fdd1f4e9114f29911592a5eb10a94bb3a13ccac3c4","impliedFormat":1},{"version":"5b7aa3c4c1a5d81b411e8cb302b45507fea9358d3569196b27eb1a27ae3a90ef","affectsGlobalScope":true,"impliedFormat":1},{"version":"5987a903da92c7462e0b35704ce7da94d7fdc4b89a984871c0e2b87a8aae9e69","affectsGlobalScope":true,"impliedFormat":1},{"version":"ea08a0345023ade2b47fbff5a76d0d0ed8bff10bc9d22b83f40858a8e941501c","impliedFormat":1},{"version":"47613031a5a31510831304405af561b0ffaedb734437c595256bb61a90f9311b","impliedFormat":1},{"version":"ae062ce7d9510060c5d7e7952ae379224fb3f8f2dd74e88959878af2057c143b","impliedFormat":1},{"version":"8a1a0d0a4a06a8d278947fcb66bf684f117bf147f89b06e50662d79a53be3e9f","affectsGlobalScope":true,"impliedFormat":1},{"version":"9f663c2f91127ef7024e8ca4b3b4383ff2770e5f826696005de382282794b127","impliedFormat":1},{"version":"9f55299850d4f0921e79b6bf344b47c420ce0f507b9dcf593e532b09ea7eeea1","impliedFormat":1},{"version":"d34aa8df2d0b18fb56b1d772ff9b3c7aea7256cf0d692f969be6e1d27b74d660","impliedFormat":1},{"version":"baac9896d29bcc55391d769e408ff400d61273d832dd500f21de766205255acb","impliedFormat":1},{"version":"2f5747b1508ccf83fad0c251ba1e5da2f5a30b78b09ffa1cfaf633045160afed","impliedFormat":1},{"version":"90407bbaa24977b8a6a90861148ac98d8652afe69992a90d823f29e9807fe2d7","affectsGlobalScope":true,"impliedFormat":1},{"version":"b71c603a539078a5e3a039b20f2b0a0d1708967530cf97dec8850a9ca45baa2b","impliedFormat":1},{"version":"d3f2d715f57df3f04bf7b16dde01dec10366f64fce44503c92b8f78f614c1769","impliedFormat":1},{"version":"cb90077223cc1365fa21ef0911a1f9b8f2f878943523d97350dc557973ca3823","impliedFormat":1},{"version":"18f1541b81b80d806120a3489af683edfb811deb91aeca19735d9bb2613e6311","impliedFormat":1},{"version":"104c67f0da1bdf0d94865419247e20eded83ce7f9911a1aa75fc675c077ca66e","impliedFormat":1},{"version":"cc0d0b339f31ce0ab3b7a5b714d8e578ce698f1e13d7f8c60bfb766baeb1d35c","impliedFormat":1},{"version":"232f118ae64ab84dcd26ddb60eaed5a6e44302d36249abf05e9e3fc2cbb701a2","impliedFormat":1},{"version":"d3cfde44f8089768ebb08098c96d01ca260b88bccf238d55eee93f1c620ff5a5","impliedFormat":1},{"version":"293eadad9dead44c6fd1db6de552663c33f215c55a1bfa2802a1bceed88ff0ec","impliedFormat":1},{"version":"833e92c058d033cde3f29a6c7603f517001d1ddd8020bc94d2067a3bc69b2a8e","impliedFormat":1},{"version":"08b2fae7b0f553ad9f79faec864b179fc58bc172e295a70943e8585dd85f600c","impliedFormat":1},{"version":"f12edf1672a94c578eca32216839604f1e1c16b40a1896198deabf99c882b340","impliedFormat":1},{"version":"e3498cf5e428e6c6b9e97bd88736f26d6cf147dedbfa5a8ad3ed8e05e059af8a","impliedFormat":1},{"version":"dba3f34531fd9b1b6e072928b6f885aa4d28dd6789cbd0e93563d43f4b62da53","impliedFormat":1},{"version":"f672c876c1a04a223cf2023b3d91e8a52bb1544c576b81bf64a8fec82be9969c","impliedFormat":1},{"version":"e4b03ddcf8563b1c0aee782a185286ed85a255ce8a30df8453aade2188bbc904","impliedFormat":1},{"version":"2329d90062487e1eaca87b5e06abcbbeeecf80a82f65f949fd332cfcf824b87b","impliedFormat":1},{"version":"25b3f581e12ede11e5739f57a86e8668fbc0124f6649506def306cad2c59d262","impliedFormat":1},{"version":"4fdb529707247a1a917a4626bfb6a293d52cd8ee57ccf03830ec91d39d606d6d","impliedFormat":1},{"version":"a9ebb67d6bbead6044b43714b50dcb77b8f7541ffe803046fdec1714c1eba206","impliedFormat":1},{"version":"5780b706cece027f0d4444fbb4e1af62dc51e19da7c3d3719f67b22b033859b9","impliedFormat":1},{"version":"75f70aa4d90fea9aaa3e4ac28c2fb79efaee2e2fa4cc2bb6cb8e2b9ed6d486c9","signature":"da052dafe066d5eb3e5d8c4ec0f93f8fcf9d875a10421e2022ddde95a41eea66","impliedFormat":1},{"version":"21247c958d397091ec30e63b27294baa1d1434c333da4fda697743190311dc62","impliedFormat":1},{"version":"26d27221c520a6781d0acc773627c8537c6bfb40561c95af2890f22ae8d342f5","impliedFormat":1},{"version":"d5eb5865d4cbaa9985cc3cfb920b230cdcf3363f1e70903a08dc4baab80b0ce1","impliedFormat":1},{"version":"51ebca098538b252953b1ef83c165f25b52271bfb6049cd09d197dddd4cd43c5","impliedFormat":1},{"version":"0652ec83ed2dc9f5dcd9ccb8a0bda9975d40f47e650c54b105840c703522119e","signature":"6ec5b58d1d9151940595e78d48b5d8ad805a6328ed4c6a7e58dfc248db2f3641","impliedFormat":1},{"version":"3b4d7cf36acdb3bc747d8e1a4f0e7969c60d2c8bc6ebb1cf07e729686341b149","signature":"dfbf279e0cded7be7f5969f05ddc109ca6654ab22f4919671f2bd2edf715e552","impliedFormat":1},{"version":"332680a9475bd631519399f9796c59502aa499aa6f6771734eec82fa40c6d654","impliedFormat":1},{"version":"191bee6605de2b5210f29f22df04f5b5e6bdcc1f6e21fb07091d40eeeb75fd72","impliedFormat":1},{"version":"d83f3c0362467589b3a65d3a83088c068099c665a39061bf9b477f16708fa0f9","impliedFormat":1},{"version":"180e527dbc1f5ae2bbb79d0a3db1ada49258783d7e6299559e0f2ed663b4afec","impliedFormat":1},{"version":"29994a97447d10d003957bcc0c9355c272d8cf0f97143eb1ade331676e860945","impliedFormat":1},{"version":"f4260022f7af38e533d364ea62eb7ae01b0a32050033d7f6772073e1dc908025","impliedFormat":1},{"version":"9cddf06f2bc6753a8628670a737754b5c7e93e2cfe982a300a0b43cf98a7d032","impliedFormat":1},{"version":"3f8e68bd94e82fe4362553aa03030fcf94c381716ce3599d242535b0d9953e49","impliedFormat":1},{"version":"63e628515ec7017458620e1624c594c9bd76382f606890c8eebf2532bcab3b7c","impliedFormat":1},{"version":"355d5e2ba58012bc059e347a70aa8b72d18d82f0c3491e9660adaf852648f032","impliedFormat":1},{"version":"311cc121259b3e0c3c08304fc25b525aa02ba0f9bf55b3e7c60b0dbb7422014e","impliedFormat":1},{"version":"74c269b43d39e5ece20b2cca49c14e64c05b01e46407200d7558301d0fcaabf4","impliedFormat":1},{"version":"ec09bd95866efe38cd00ebb79dfa7a26563d600fa4a30db0f7c6d68f8f6d2b06","impliedFormat":1},{"version":"482d0ac70d56aa79941be30da6df28e926a007f835eed70cf7b5f3135368d1f6","impliedFormat":1},{"version":"7dd19397d5a090c9f8cd762bae67bd0ad6f782abe422594fb71168fb578673b0","impliedFormat":1},{"version":"84cbf6204ada0ee2f80493e55e45befa079954788718efd6dcc103183104e3c0","impliedFormat":1},{"version":"ed849d616865076f44a41c87f27698f7cdf230290c44bafc71d7c2bc6919b202","impliedFormat":1},{"version":"9a0a0af04065ddfecc29d2b090659fce57f46f64c7a04a9ba63835ef2b2d0efa","impliedFormat":1},{"version":"10297d22a9209a718b9883a384db19249b206a0897e95f2b9afeed3144601cb0","impliedFormat":1},{"version":"034b8b5912823744c986986f24432bf3fa7bfa671e69316b672f3f2db5166ce4","impliedFormat":1},{"version":"34d206f6ba993e601dade2791944bdf742ab0f7a8caccc661106c87438f4f904","impliedFormat":1},{"version":"05ca49cc7ba9111f6c816ecfadb9305fffeb579840961ee8286cc89749f06ebd","impliedFormat":1},{"version":"187b4a56867783a12f29f1b9ec217f24cb6dc3b819efd3c069378d0ad9fa6ac8","signature":"db57b0d65101c45015e38bf4d3428fae08434b18b8f3ef4b37fec3139ccaa503","impliedFormat":1},{"version":"569e762cf47aafdad508360a443c6c757e56c61db3b652b65458a7d168d139c4","impliedFormat":1},{"version":"02ed2766d79a00719ac3cc77851d54bd7197c1b12085ea12126bc2a65068223e","impliedFormat":1},{"version":"4b84373e192b7e0f8569b65eb16857098a6ee279b75d49223db2a751fdd7efde","impliedFormat":1},{"version":"5aeea312cd1d3cc5d72fc8a9c964439d771bdf41d9cce46667471b896b997473","impliedFormat":1},{"version":"cfa7bf135cafc5aad7cf544bc1cebf65a1fdb4373223cc85ea7d7196e18be151","impliedFormat":1},{"version":"f2c4a36eb216aadb0d9c79862a31b922ccfa1eaaa38d2124cc9192d40eda4779","impliedFormat":1},{"version":"cb5bb1db16ff4b534f56f7741e7ffd0a007ce36d387a377d4c196036e0932423","impliedFormat":1},{"version":"25be1eb939c9c63242c7a45446edb20c40541da967f43f1aa6a00ed53c0552db","impliedFormat":1},{"version":"08c2bb524b8ed271f194e1c7cc6ad0bcc773f596c41f68a207d0ec02c9727060","impliedFormat":1},{"version":"012b69bc8a16a21aa0863502339c49258c579723f9e7a54faa5f0d5c2b1ae1b7","impliedFormat":1},{"version":"29ad73d9e365d7b046f3168c6a510477bfe30d84a71cd7eb2f0e555b1d63f5f6","impliedFormat":1},{"version":"d99e9f5aa43397599fe824e38c33d13d3a9e19198806a4363114bd7ac58b29cc","impliedFormat":1},{"version":"440099416057789b14f85af057d4924915f27043399c10d4ca67409d94b963cf","impliedFormat":1},{"version":"ac44995fc7d0781d77927bae7dd41a31f0309e695fd2694b175c0ce3bc4b3b50","impliedFormat":1},{"version":"0c1f802f7a60ca8084e5188ac7952accdfc00f39ded3ebbbd3cdcc9da51b9a7b","impliedFormat":1},{"version":"a32e3fc530d8d1a18bf54678d8d55714827a50c9fabdd4ede7155a56be7d1dcb","impliedFormat":1},{"version":"14ecfc29e0c44ad4c5e50f9b597492cd8f45a2a635db8b5fe911a5da83e26cf8","impliedFormat":1},{"version":"06e9dc3f7549e194e0ed6e46e4ac52dee84bb5973f1e96edc2adff83ff6e6e5f","impliedFormat":1},{"version":"c2f041fe0e7ae2d5a19c477d19e8ec13de3d65ef45e442fa081cf6098cdcbe2d","impliedFormat":1},{"version":"0cef678147928ef223ff7f2aae3442cc9f4e9996956e9ac92434e626d0e147f8","impliedFormat":1},{"version":"198ae766bb698feb66d3188cfce59fb33696c951b10f901aa3fc3db4847ce76a","impliedFormat":1},{"version":"6dc488fd3d01e4269f0492b3e0ee7961eec79f4fc3ae997c7d28cde0572dbd91","impliedFormat":1},{"version":"a09b706f16bda9372761bd70cf59814b6f0a0c2970d62a5b2976e2fd157b920f","impliedFormat":1},{"version":"70da4bfde55d1ec74e3aa7635eae741f81ced44d3c344e2d299e677404570ca9","impliedFormat":1},{"version":"bf4f6b0d2ae8d11dc940c20891f9a4a558be906a530b9d9a8ff1032afa1962cd","impliedFormat":1},{"version":"9975431639f84750a914333bd3bfa9af47f86f54edbaa975617f196482cfee31","impliedFormat":1},{"version":"70a5cb56f988602271e772c65cb6735039148d5e90a4c270e5806f59fc51d3a0","impliedFormat":1},{"version":"e083384623f90cfa7e8d2aa7efe78c51095a04ad51d1f82c3e4052689666895d","impliedFormat":1},{"version":"769b6db9a392113983977bf5759bdc7c80e1ec1b37a49a0f413d7bcb3ef58081","signature":"e2365738e461d5d68faa9a66ab0405fd4244abf2712f419cfa310f5583306574","impliedFormat":1},{"version":"f06a1129cf403bdeb1d8bcdf3bfae3bea34a3a351261a3e0656180d2cd187bd0","impliedFormat":1},{"version":"7ebc900cb29387693f29e5d64670d0525bfaaf712263c70a9ce390aa705bec59","signature":"25ef84af75af3e1dd36df6e8e1e6b427b9abc31ad43c41929bcad2d6cd887fa5","impliedFormat":1},{"version":"dfce39e9cdb3ceb0e9c2d038025f9739ed3fcce0120cd38d8b24011c381b2e34","signature":"8bc5a0f0d98ccd586f5ce35c2a85de67951a28384faa60bcd33dcbabf5911f82","impliedFormat":1},{"version":"58481c938f3ccdab42c308b481e716426eb2e27525aa3d4b58dd51562908a554","signature":"fe9c9e94d3f43d35857666d0d778e62274b38138f1809409e27f66300bfadd57","impliedFormat":1},{"version":"f04dd72652bd6b57565a52d09c6a0199baff01f3b27bc2c2ffd3870b7794fc21","signature":"2dd7fed33720f2e77b7a43aced1369f58d7c5ff00a503ba09973cf9a76eee64b","impliedFormat":1},{"version":"0e11913a60ea59f0b431fe85334d58c83b25e401c123364624b3fed5af0f6e40","signature":"97e7ad4d8574bfb74db56bcc71e74c9660b17e7765d1f9467bf3430abbbd2060","impliedFormat":1},{"version":"f874ea4d0091b0a44362a5f74d26caab2e66dec306c2bf7e8965f5106e784c3b","impliedFormat":1},{"version":"4d8d42fa8ecf15c313721de52744d83ea736ad9646ba9e9cdf5f9489f48d746d","signature":"040c2a8dcc697941d2de76a033e777827f16b3d830376ecde1c56053f2ca1423","impliedFormat":1},{"version":"3085ac44e61de6a0f69c6d36d075db789222a0d3aa0096a460931454099fd1b2","signature":"465f62cee1923cc7f71ed4f5577ab503cd92d6f464e9b9694d4e0da5ceaf682c","impliedFormat":1},{"version":"2556e7e8bb7e6f0bb3fe25f3da990d1812cb91f8c9b389354b6a0c8a6d687590","impliedFormat":1},{"version":"ad1c91ca536e0962dcbfcdff40073e3dd18da839e0baad3fe990cf0d10c93065","impliedFormat":1},{"version":"19cf605ba2a4e8fba017edebdddbbc45aea897ddc58b4aae4c55f382b570ff53","impliedFormat":1},{"version":"884aab8c07224434c034b49e88de0511f21536aa83ee88f1285160ba6d3fb77a","impliedFormat":1},{"version":"130b39b18c99e5678635f383ef57efaa507196838ddabb47cb104064e2ce4cd3","impliedFormat":1},{"version":"7618d2cb769e2093acd4623d645b683ab9fea78c262b3aa354aba9f5afdcaaee","impliedFormat":1},{"version":"029f1ce606891c3f57f4c0c60b8a46c8ced53e719d27a7c9693817f2fe37690b","impliedFormat":1},{"version":"83596c963e276a9c5911412fba37ae7c1fe280f2d77329928828eed5a3bfa9a6","impliedFormat":1},{"version":"81acfd3a01767770e559bc57d32684756989475be6ea32e2fe6255472c3ea116","impliedFormat":1},{"version":"88d0c3eae81868b4749ba5b88f9b6d564ee748321ce19a2f4269a4e9dd46020a","impliedFormat":1},{"version":"8266b39a828bfb2695cabfa403e7c1226d7d94599f21bea9f760e35f4ca7a576","impliedFormat":1},{"version":"c1c1e740195c882a776cf084acbaf963907785ee39e723c6375fec9a59bf2387","impliedFormat":1},{"version":"137f96b78e477e08876f6372072c3b6f1767672bf182013f84f8ae53d987ff86","impliedFormat":1},{"version":"29896c61d09880ff39f8a86873bf72ce4deb910158d3a496122781e29904c615","impliedFormat":1},{"version":"81ce540acef0d6972b0b163331583181be3603300f618dcd6a6a3138954ff30c","impliedFormat":1},{"version":"19990350fca066265b2c190c9b6cde1229f35002ea2d4df8c9e397e9942f6c89","impliedFormat":1},{"version":"8fb8fdda477cd7382477ffda92c2bb7d9f7ef583b1aa531eb6b2dc2f0a206c10","impliedFormat":1},{"version":"66995b0c991b5c5d42eff1d950733f85482c7419f7296ab8952e03718169e379","impliedFormat":1},{"version":"9863f888da357e35e013ca3465b794a490a198226bd8232c2f81fb44e16ff323","impliedFormat":1},{"version":"84bc2d80326a83ee4a6e7cba2fd480b86502660770c0e24da96535af597c9f1e","impliedFormat":1},{"version":"ea27768379b866ee3f5da2419650acdb01125479f7af73580a4bceb25b79e372","impliedFormat":1},{"version":"598931eeb4362542cae5845f95c5f0e45ac668925a40ce201e244d7fe808e965","impliedFormat":1},{"version":"da9ef88cde9f715756da642ad80c4cd87a987f465d325462d6bc2a0b11d202c8","impliedFormat":1},{"version":"b4c6184d78303b0816e779a48bef779b15aea4a66028eb819aac0abee8407dea","impliedFormat":1},{"version":"db085d2171d48938a99e851dafe0e486dce9859e5dfa73c21de5ed3d4d6fb0c5","impliedFormat":1},{"version":"62a3ad1ddd1f5974b3bf105680b3e09420f2230711d6520a521fab2be1a32838","impliedFormat":1},{"version":"a77be6fc44c876bc10c897107f84eaba10790913ebdcad40fcda7e47469b2160","impliedFormat":1},{"version":"06cf55b6da5cef54eaaf51cdc3d4e5ebf16adfdd9ebd20cec7fe719be9ced017","impliedFormat":1},{"version":"91f5dbcdb25d145a56cffe957ec665256827892d779ef108eb2f3864faff523b","impliedFormat":1},{"version":"052ba354bab8fb943e0bc05a0769f7b81d7c3b3c6cd0f5cfa53c7b2da2a525c5","impliedFormat":1},{"version":"927955a3de5857e0a1c575ced5a4245e74e6821d720ed213141347dd1870197f","impliedFormat":1},{"version":"fec804d54cd97dd77e956232fc37dc13f53e160d4bbeeb5489e86eeaa91f7ebd","impliedFormat":1},{"version":"75ef949153a3e6ff419e39d0fa5eb6617e92de5019738ad3c43872023d9665f5","impliedFormat":1},{"version":"ed9ce8e6dd5b2d00ab95efc44e4ad9d0eba77362e01619cb21dedfdedbad51b8","impliedFormat":1},{"version":"5520611f997f2b8e62a6e191da45b07813ac2e758304690606604a64ac0ca976","impliedFormat":1},{"version":"00b469cba48c9d772a4555216d21ba41cdb5a732af797ccb57267344f4fc6c3d","impliedFormat":1},{"version":"2766bf77766c85c25ec31586823fefb48344e64556faad7e75a3363e517814f6","impliedFormat":1},{"version":"b7d1eaffd8003e8dc0ec275e58bd24c7b9a4dbae2a2d0d83cf248c88237262ce","impliedFormat":1},{"version":"7a8b08c0521c3a9e1db3c8b14f37e59d838fdc32389f1193b96630b435a8e64e","impliedFormat":1},{"version":"2e54848617fae9eb73654d9cf4295d99dab4b9c759934e5b82e2e57e6aaaef20","impliedFormat":1},{"version":"ae056b7c3f727d492166d4c1169d5905ddd194128a014b5d2d621248ed94b49c","impliedFormat":1},{"version":"edc5d99a04130f066f6e8d31c7c3f9ba4749496356470279408833b4faee3554","impliedFormat":1},{"version":"2f502ac2473a2bbf0d6217f9660e9d5bf40165a2f91067596323898c53dab87c","impliedFormat":1},{"version":"21f27a0c8bc8d9a4e2cf6d9c60140f8b071d0e1ffddb4b7dcf6bbf74d0e8d470","impliedFormat":1},{"version":"754108a1e136331ac67dc8ee6aa9c95cb3bea3ac8bbf48dda7b0dbabbc8f970f","impliedFormat":1},{"version":"9e9979adc151111d71ad049305be1b6df324a98d1d1edd84adb1756cc1911bfd","impliedFormat":1},{"version":"0f38bcf19f105cd31ded5d46491ca50a46462c838816c358d445f41ac7a68f5a","impliedFormat":1},{"version":"a65fc667cd78d7cad733fab96f4bff3183c0dcbc15b083dce0055cffc5c64f9f","impliedFormat":1},{"version":"c735e27dfa775155120c50f714f594639dd7b6ad1878097feb005a0b5c59b7c2","impliedFormat":1},{"version":"f3dd541f4d87bba38dabf43fd06b7616c6f86b11608d30e61086ab39f84fa8d8","impliedFormat":1},{"version":"5583f1c0912e96625a30c20b83cff3d175194b222e4eb22170d19e33f7d8729f","impliedFormat":1},{"version":"a515b08047d24de84d89ad80b2843e565e65ed4a4e7cfc9707656470d7c555f9","impliedFormat":1},{"version":"cf43b2783a58e42fca6e45f0d47465b2ab855b7e9bea5ccb68447297df8aade5","impliedFormat":1},{"version":"27a3f158d8e6f59f29e55c37d4ae3c39574ee99539c4f12bcf46d29929974a62","impliedFormat":1},{"version":"a2d23e2f22006483c89f42077bd6a9bf92db721ebb5e0859b06fdb5c8369586d","impliedFormat":1},{"version":"6a8aec6851c09e4524937485f6553ec7332118482f3ed33238cea7496ff42103","impliedFormat":1},{"version":"d67fd6ea8cf37131627c7a9ae1de96d19d41cb32e741a475f0f56942576a7b3b","impliedFormat":1},{"version":"9b2f424a2c5c592d738100d898df3f9ee018bdd23a279f10849c3686abbec158","impliedFormat":1},{"version":"2fef96aedd23d59b6093d12d9f97c95e3a4008fcc02e8c68304235a1770fc70a","impliedFormat":1},{"version":"cdcf9ea426ad970f96ac930cd176d5c69c6c24eebd9fc580e1572d6c6a88f62c","impliedFormat":1},{"version":"23cd712e2ce083d68afe69224587438e5914b457b8acf87073c22494d706a3d0","impliedFormat":1},{"version":"156a859e21ef3244d13afeeba4e49760a6afa035c149dda52f0c45ea8903b338","impliedFormat":1},{"version":"10ec5e82144dfac6f04fa5d1d6c11763b3e4dbbac6d99101427219ab3e2ae887","impliedFormat":1},{"version":"615754924717c0b1e293e083b83503c0a872717ad5aa60ed7f1a699eb1b4ea5c","impliedFormat":1},{"version":"074de5b2fdead0165a2757e3aaef20f27a6347b1c36adea27d51456795b37682","impliedFormat":1},{"version":"68834d631c8838c715f225509cfc3927913b9cc7a4870460b5b60c8dbdb99baf","impliedFormat":1},{"version":"21adf13435b9b748529c8cedf80f884e5130b9684188120a686cd2b26a2059c7","impliedFormat":1},{"version":"ccab02f3920fc75c01174c47fcf67882a11daf16baf9e81701d0a94636e94556","impliedFormat":1},{"version":"170431a9a3180034439e806b68e192e35d58ce104b42d19ee67217d3729c96e0","impliedFormat":1},{"version":"ea6bc8de8b59f90a7a3960005fd01988f98fd0784e14bc6922dde2e93305ec7d","impliedFormat":1},{"version":"36107995674b29284a115e21a0618c4c2751b32a8766dd4cb3ba740308b16d59","impliedFormat":1},{"version":"914a0ae30d96d71915fc519ccb4efbf2b62c0ddfb3a3fc6129151076bc01dc60","impliedFormat":1},{"version":"9c32412007b5662fd34a8eb04292fb5314ec370d7016d1c2fb8aa193c807fe22","impliedFormat":1},{"version":"7fd1b31fd35876b0aa650811c25ec2c97a3c6387e5473eb18004bed86cdd76b6","impliedFormat":1},{"version":"4d327f7d72ad0918275cea3eee49a6a8dc8114ae1d5b7f3f5d0774de75f7439a","impliedFormat":1},{"version":"6ebe8ebb8659aaa9d1acbf3710d7dae3e923e97610238b9511c25dc39023a166","impliedFormat":1},{"version":"e85d7f8068f6a26710bff0cc8c0fc5e47f71089c3780fbede05857331d2ddec9","impliedFormat":1},{"version":"7befaf0e76b5671be1d47b77fcc65f2b0aad91cc26529df1904f4a7c46d216e9","impliedFormat":1},{"version":"0a60a292b89ca7218b8616f78e5bbd1c96b87e048849469cccb4355e98af959a","impliedFormat":1},{"version":"0b6e25234b4eec6ed96ab138d96eb70b135690d7dd01f3dd8a8ab291c35a683a","impliedFormat":1},{"version":"9666f2f84b985b62400d2e5ab0adae9ff44de9b2a34803c2c5bd3c8325b17dc0","impliedFormat":1},{"version":"40cd35c95e9cf22cfa5bd84e96408b6fcbca55295f4ff822390abb11afbc3dca","impliedFormat":1},{"version":"b1616b8959bf557feb16369c6124a97a0e74ed6f49d1df73bb4b9ddf68acf3f3","impliedFormat":1},{"version":"5b03a034c72146b61573aab280f295b015b9168470f2df05f6080a2122f9b4df","impliedFormat":1},{"version":"40b463c6766ca1b689bfcc46d26b5e295954f32ad43e37ee6953c0a677e4ae2b","impliedFormat":1},{"version":"249b9cab7f5d628b71308c7d9bb0a808b50b091e640ba3ed6e2d0516f4a8d91d","impliedFormat":1},{"version":"80aae6afc67faa5ac0b32b5b8bc8cc9f7fa299cff15cf09cc2e11fd28c6ae29e","impliedFormat":1},{"version":"f473cd2288991ff3221165dcf73cd5d24da30391f87e85b3dd4d0450c787a391","impliedFormat":1},{"version":"499e5b055a5aba1e1998f7311a6c441a369831c70905cc565ceac93c28083d53","impliedFormat":1},{"version":"8aee8b6d4f9f62cf3776cda1305fb18763e2aade7e13cea5bbe699112df85214","impliedFormat":1},{"version":"98498b101803bb3dde9f76a56e65c14b75db1cc8bec5f4db72be541570f74fc5","impliedFormat":1},{"version":"1cc2a09e1a61a5222d4174ab358a9f9de5e906afe79dbf7363d871a7edda3955","impliedFormat":1},{"version":"5d0375ca7310efb77e3ef18d068d53784faf62705e0ad04569597ae0e755c401","impliedFormat":1},{"version":"59af37caec41ecf7b2e76059c9672a49e682c1a2aa6f9d7dc78878f53aa284d6","impliedFormat":1},{"version":"addf417b9eb3f938fddf8d81e96393a165e4be0d4a8b6402292f9c634b1cb00d","impliedFormat":1},{"version":"436d7b4543b340b0f3eef4310d524242e41369b9652aa9c70428767c4dcac455","impliedFormat":1},{"version":"adf27937dba6af9f08a68c5b1d3fce0ca7d4b960c57e6d6c844e7d1a8e53adae","impliedFormat":1},{"version":"12950411eeab8563b349cb7959543d92d8d02c289ed893d78499a19becb5a8cc","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"c9381908473a1c92cb8c516b184e75f4d226dad95c3a85a5af35f670064d9a2f","impliedFormat":1},{"version":"b3fb72492a07a76f7bfa29ecadd029eea081df11512e4dfe6f930a5a9cb1fb75","impliedFormat":1},{"version":"66b99e58194754a9db09e8ae09b45f878a5041f9ba497feaa9db8cc9773cafcc","impliedFormat":1},{"version":"4b061f6759a5737c51a5207b3729a7efdfa47bac905a918b2b6795abc35df2cb","signature":"5caaa5064fa7f953780f45d51e0c3c5224e705514b91adfa5064a3a3f5ebb17a","impliedFormat":1},{"version":"515d1090f89ec9424863ddfd9543ee81827fe1bd4a13fc850c6b9e34f82d9bcd","signature":"975d6797a50a7a15de09a75a05b38d6c06182a5130326bfdb558ceada707e133","impliedFormat":1},{"version":"6e0d6332efa06df8f3cd802af5a52c01368817b3eccd5cc31eb551a8142d97fe","signature":"c106970ee4b686b5ebf0964ea4e0d1b2af02bdecd2d5cfeecedeb7bb5b2f404b","impliedFormat":1},{"version":"6ce6a173509cf5ba88395a78298fc9701bdc81fcc9bd17691e28ee9ec9032a56","signature":"5ada4fb5c131c2349786292abdee93cc9244aad5a63f5301e6b37e3e1fba0c62","impliedFormat":1},{"version":"a2fbe8ace23651e4e99a89d73e7b8e7d6757b92e72f2e9a7e5bb8ae8c7a2afb4","signature":"c106970ee4b686b5ebf0964ea4e0d1b2af02bdecd2d5cfeecedeb7bb5b2f404b","impliedFormat":1},{"version":"5697894055830fea97f4edcbd8728c0b60aa9506c43bda4b3654cb7b55ac3e5d","signature":"c106970ee4b686b5ebf0964ea4e0d1b2af02bdecd2d5cfeecedeb7bb5b2f404b","impliedFormat":1},{"version":"1e8565c9cd01bf554fe5b4f8d38def23b778b80ebb9525be988e73d222421932","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1}],"root":[187,192,193,216,245,[247,251],253,254,[358,364]],"options":{"declaration":true,"declarationMap":true,"esModuleInterop":true,"exactOptionalPropertyTypes":false,"module":199,"noImplicitAny":true,"noImplicitReturns":true,"noUncheckedIndexedAccess":true,"noUnusedLocals":true,"noUnusedParameters":true,"outDir":"./dist","rootDir":"./src","skipLibCheck":true,"sourceMap":true,"strict":true,"target":9},"referencedMap":[[190,1],[189,2],[191,3],[188,4],[233,4],[171,5],[170,6],[224,6],[165,7],[172,8],[166,4],[167,4],[108,9],[109,9],[110,10],[63,11],[111,12],[112,13],[113,14],[58,4],[61,15],[59,4],[60,4],[114,16],[115,17],[116,18],[117,19],[118,20],[119,21],[120,21],[121,22],[122,23],[123,24],[124,25],[64,4],[62,4],[125,26],[126,27],[127,28],[161,29],[128,30],[129,4],[130,31],[131,32],[132,33],[133,34],[134,35],[135,36],[136,37],[137,38],[138,39],[139,39],[140,40],[141,4],[142,41],[143,42],[145,43],[144,44],[146,45],[147,46],[148,47],[149,48],[150,49],[151,50],[152,51],[153,52],[154,53],[155,54],[156,55],[157,56],[158,57],[65,4],[66,4],[67,4],[105,58],[106,4],[107,4],[159,59],[160,60],[163,4],[164,4],[162,61],[169,62],[168,63],[252,4],[298,64],[300,65],[303,65],[305,66],[304,65],[302,67],[301,67],[306,68],[357,69],[310,70],[309,71],[299,72],[311,73],[308,74],[307,65],[297,75],[295,4],[293,76],[296,77],[294,78],[292,79],[291,80],[289,81],[290,81],[288,4],[210,4],[260,82],[255,4],[257,83],[256,84],[267,82],[266,82],[268,85],[265,86],[263,82],[264,82],[261,87],[262,82],[313,88],[312,4],[217,4],[219,89],[218,89],[220,90],[225,4],[232,91],[221,4],[226,92],[223,93],[222,94],[230,95],[227,96],[228,96],[229,97],[231,98],[246,99],[269,100],[259,101],[258,4],[208,102],[209,103],[207,104],[195,105],[200,106],[201,107],[204,108],[203,109],[202,110],[205,111],[212,112],[215,113],[214,114],[213,115],[206,116],[196,117],[211,118],[198,119],[194,120],[199,121],[197,105],[287,122],[272,123],[285,124],[270,4],[271,125],[286,126],[281,127],[282,128],[280,129],[284,130],[278,131],[273,132],[283,133],[279,124],[276,4],[277,134],[274,4],[275,4],[238,135],[237,30],[239,136],[234,137],[241,138],[236,139],[244,140],[243,141],[240,142],[242,143],[235,30],[56,4],[57,4],[11,4],[10,4],[2,4],[12,4],[13,4],[14,4],[15,4],[16,4],[17,4],[18,4],[19,4],[3,4],[20,4],[21,4],[4,4],[22,4],[26,4],[23,4],[24,4],[25,4],[27,4],[28,4],[29,4],[5,4],[30,4],[31,4],[32,4],[33,4],[6,4],[37,4],[34,4],[35,4],[36,4],[38,4],[7,4],[39,4],[44,4],[45,4],[40,4],[41,4],[42,4],[43,4],[8,4],[49,4],[46,4],[47,4],[48,4],[50,4],[9,4],[51,4],[52,4],[53,4],[55,4],[54,4],[1,4],[83,144],[93,145],[82,144],[103,146],[74,147],[73,148],[102,149],[96,150],[101,151],[76,152],[90,153],[75,154],[99,155],[71,156],[70,149],[100,157],[72,158],[77,159],[78,4],[81,159],[68,4],[104,160],[94,161],[85,162],[86,163],[88,164],[84,165],[87,166],[97,149],[79,167],[80,168],[89,169],[69,170],[92,161],[91,159],[95,4],[98,171],[356,172],[332,173],[344,174],[329,175],[345,170],[354,176],[320,177],[321,178],[319,148],[353,149],[348,179],[352,180],[323,181],[341,182],[322,183],[351,184],[317,185],[318,179],[324,186],[325,4],[331,187],[328,186],[315,188],[355,189],[346,190],[335,191],[334,186],[336,192],[339,193],[333,194],[337,195],[349,149],[326,196],[327,197],[340,198],[316,170],[343,199],[342,186],[330,197],[338,200],[347,4],[314,4],[350,201],[186,202],[178,203],[185,204],[180,4],[181,4],[179,205],[182,206],[173,4],[174,4],[175,202],[177,207],[183,4],[184,208],[176,209],[193,210],[187,211],[216,212],[245,213],[364,214],[250,215],[249,216],[248,217],[251,218],[360,219],[363,220],[362,221],[253,222],[254,223],[359,224],[358,225],[361,226],[247,4],[192,4]],"semanticDiagnosticsPerFile":[[253,[{"start":164,"length":10,"messageText":"'DataSource' is declared but never used.","category":1,"code":6196,"reportsUnnecessary":true}]],[358,[{"start":1459,"length":8,"code":2322,"category":1,"messageText":{"messageText":"Type 'string | undefined' is not assignable to type 'string | null'.","category":1,"code":2322,"next":[{"messageText":"Type 'undefined' is not assignable to type 'string | null'.","category":1,"code":2322}]}},{"start":4699,"length":21,"messageText":"Not all code paths return a value.","category":1,"code":7030},{"start":6419,"length":14,"code":2345,"category":1,"messageText":{"messageText":"Argument of type 'string | undefined' is not assignable to parameter of type 'string'.","category":1,"code":2345,"next":[{"messageText":"Type 'undefined' is not assignable to type 'string'.","category":1,"code":2322}]}}]],[359,[{"start":1515,"length":4,"code":2322,"category":1,"messageText":{"messageText":"Type '{ status?: JobStatus | undefined; progress?: number | undefined; step?: string | undefined; result?: unknown; error?: string | undefined; }' is not assignable to type '(Without & ScrapeJobUncheckedUpdateInput) | (Without<...> & ScrapeJobUpdateInput)'.","category":1,"code":2322,"next":[{"messageText":"Type '{ status?: JobStatus | undefined; progress?: number | undefined; step?: string | undefined; result?: unknown; error?: string | undefined; }' is not assignable to type 'Without & ScrapeJobUpdateInput'.","category":1,"code":2322,"next":[{"messageText":"Type '{ status?: JobStatus | undefined; progress?: number | undefined; step?: string | undefined; result?: unknown; error?: string | undefined; }' is not assignable to type 'ScrapeJobUpdateInput'.","category":1,"code":2322,"next":[{"messageText":"Types of property 'result' are incompatible.","category":1,"code":2326,"next":[{"messageText":"Type 'unknown' is not assignable to type 'NullableJsonNullValueInput | InputJsonValue | undefined'.","category":1,"code":2322,"canonicalHead":{"code":2322,"messageText":"Type '{ status?: JobStatus | undefined; progress?: number | undefined; step?: string | undefined; result?: unknown; error?: string | undefined; }' is not assignable to type 'ScrapeJobUpdateInput'."}}]}]}]}]},"relatedInformation":[{"file":"./node_modules/.prisma/client/index.d.ts","start":201674,"length":4,"messageText":"The expected type comes from property 'data' which is declared here on type '{ select?: ScrapeJobSelect | null | undefined; data: (Without & ScrapeJobUncheckedUpdateInput) | (Without<...> & ScrapeJobUpdateInput); where: ScrapeJobWhereUniqueInput; }'","category":3,"code":6500}]}]],[360,[{"start":984,"length":114,"code":2769,"category":1,"messageText":{"messageText":"No overload matches this call.","category":1,"code":2769,"next":[{"messageText":"The last overload gave the following error.","category":1,"code":2770,"next":[{"messageText":"Argument of type '(req: Request & { validated: GetInfoRequest; }, res: Response>) => Promise' is not assignable to parameter of type 'RequestHandlerParams>'.","category":1,"code":2345,"next":[{"messageText":"Type '(req: Request & { validated: GetInfoRequest; }, res: Response>) => Promise' is not assignable to type 'RequestHandler>'.","category":1,"code":2322,"next":[{"messageText":"Types of parameters 'req' and 'req' are incompatible.","category":1,"code":2328,"next":[{"messageText":"Type 'Request>' is not assignable to type 'Request> & { validated: GetInfoRequest; }'.","category":1,"code":2322,"next":[{"messageText":"Property 'validated' is missing in type 'Request>' but required in type '{ validated: GetInfoRequest; }'.","category":1,"code":2741,"canonicalHead":{"code":2322,"messageText":"Type 'Request>' is not assignable to type '{ validated: GetInfoRequest; }'."}}]}]}]}]}]}]},"relatedInformation":[{"start":1013,"length":9,"messageText":"'validated' is declared here.","category":3,"code":2728},{"file":"./node_modules/@types/express-serve-static-core/index.d.ts","start":5182,"length":369,"messageText":"The last overload is declared here.","category":1,"code":2771}]},{"start":3533,"length":132,"code":2769,"category":1,"messageText":{"messageText":"No overload matches this call.","category":1,"code":2769,"next":[{"messageText":"The last overload gave the following error.","category":1,"code":2770,"next":[{"messageText":"Argument of type '(req: Request & { validated: GetInfoRequest; }, res: Response>) => Promise' is not assignable to parameter of type 'RequestHandlerParams>'.","category":1,"code":2345,"next":[{"messageText":"Type '(req: Request & { validated: GetInfoRequest; }, res: Response>) => Promise' is not assignable to type 'RequestHandler>'.","category":1,"code":2322,"next":[{"messageText":"Types of parameters 'req' and 'req' are incompatible.","category":1,"code":2328,"next":[{"messageText":"Type 'Request>' is not assignable to type 'Request> & { validated: GetInfoRequest; }'.","category":1,"code":2322,"next":[{"messageText":"Property 'validated' is missing in type 'Request>' but required in type '{ validated: GetInfoRequest; }'.","category":1,"code":2741,"canonicalHead":{"code":2322,"messageText":"Type 'Request>' is not assignable to type '{ validated: GetInfoRequest; }'."}}]}]}]}]}]}]},"relatedInformation":[{"start":3562,"length":9,"messageText":"'validated' is declared here.","category":3,"code":2728},{"file":"./node_modules/@types/express-serve-static-core/index.d.ts","start":5182,"length":369,"messageText":"The last overload is declared here.","category":1,"code":2771}]}]],[361,[{"start":3987,"length":4,"messageText":"'data' is of type 'unknown'.","category":1,"code":18046},{"start":4616,"length":4,"messageText":"'data' is of type 'unknown'.","category":1,"code":18046},{"start":7683,"length":4,"code":2322,"category":1,"messageText":"Type 'unknown' is not assignable to type 'TmdbRawResponse'."},{"start":9372,"length":4,"code":2322,"category":1,"messageText":"Type 'unknown' is not assignable to type 'TmdbRawResponse'."},{"start":10709,"length":4,"code":2322,"category":1,"messageText":"Type 'unknown' is not assignable to type 'TmdbRawResponse'."},{"start":10858,"length":25,"messageText":"This comparison appears to be unintentional because the types '\"movie\" | \"tv\" | undefined' and '\"person\"' have no overlap.","category":1,"code":2367}]],[364,[{"start":1142,"length":3,"messageText":"'res' is declared but its value is never read.","category":1,"code":6133,"reportsUnnecessary":true}]]],"version":"5.9.3"} \ No newline at end of file