diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 000000000..3eb13143c --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 3728529b4..220b250c3 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,10 @@ veza-backend-api/api veza-backend-api/migrate_tool chat_exports/!veza-stream-server/src/bin/ !veza-stream-server/.env + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ diff --git a/apps/web/AUDIT_PRODUCTION_FRONTEND.md b/apps/web/AUDIT_PRODUCTION_FRONTEND.md new file mode 100644 index 000000000..f3294b05f --- /dev/null +++ b/apps/web/AUDIT_PRODUCTION_FRONTEND.md @@ -0,0 +1,128 @@ +# 🔍 Audit de Production - Frontend Veza + +**Date** : 2025-01-27 +**Cible** : `apps/web` (Frontend React/Vite/TypeScript) +**Objectif** : Identification des bloquants pour un dĂ©ploiement en production stable et sĂ©curisĂ© + +--- + +## 📊 RĂ©sumĂ© ExĂ©cutif + +### Score Global : **62/100** ⚠ + +**Verdict** : **NON PRÊT POUR LA PRODUCTION** + +Le frontend prĂ©sente plusieurs problĂšmes critiques de sĂ©curitĂ© et de qualitĂ© qui doivent ĂȘtre rĂ©solus avant tout dĂ©ploiement en production. + +### Points Forts ✅ + +1. **Architecture** : Routes protĂ©gĂ©es correctement implĂ©mentĂ©es avec `ProtectedRoute` +2. **TypeScript** : Configuration TypeScript prĂ©sente et fonctionnelle +3. **Build** : Configuration Vite optimisĂ©e avec chunk splitting et sourcemaps +4. **Tests** : Infrastructure de tests prĂ©sente (Vitest + Playwright) + +### Points Faibles Majeurs 🔮 + +1. **SĂ©curitĂ© Critique** : + - CSP avec `unsafe-inline` et `unsafe-eval` (CRITICAL) + - Tokens JWT stockĂ©s dans `localStorage` (vulnĂ©rable au XSS) + - Utilisation de `dangerouslySetInnerHTML` sans sanitisation complĂšte + +2. **QualitĂ© du Code** : + - 122+ `console.log/error/warn` qui pollueront les logs de production + - 480+ utilisations de `any` (perte de sĂ©curitĂ© de type) + - Hardcoding de `localhost/127.0.0.1` dans plusieurs fichiers + +3. **Gestion d'Erreur** : + - Nombreux `catch` qui loggent uniquement sans informer l'utilisateur + - Erreurs API silencieuses dans certains composants + +4. **Dette Technique** : + - 11 TODO/FIXME/HACK identifiĂ©s + - Code temporaire (`console.log` avec commentaire "Temporary") + +--- + +## 📋 DĂ©tail des ProblĂšmes par CatĂ©gorie + +### 🔮 CRITICAL (Bloquant Production) + +1. **CSP avec unsafe-inline/unsafe-eval** (`vite.config.ts:64`) + - Impact : VulnĂ©rabilitĂ© XSS, injection de scripts + - Fix : Utiliser des nonces CSP stricts, supprimer `unsafe-eval` + +2. **Tokens dans localStorage** (Multiple fichiers) + - Impact : Vol de tokens via XSS + - Fix : Migrer vers httpOnly cookies ou sessionStorage avec rotation + +### 🟠 HIGH (Risque SĂ©curitĂ©/StabilitĂ©) + +3. **dangerouslySetInnerHTML sans sanitisation complĂšte** (2 occurrences) + - Impact : Risque XSS si sanitisation Ă©choue + - Fix : VĂ©rifier `sanitizeChatMessage`, considĂ©rer une alternative + +4. **Gestion d'erreur silencieuse** (Multiple fichiers) + - Impact : UX dĂ©gradĂ©e, bugs non visibles + - Fix : Afficher des toasts/notifications pour toutes les erreurs utilisateur + +5. **Hardcoding localhost** (Multiple fichiers) + - Impact : Build de production avec URLs de dev + - Fix : Utiliser uniquement `import.meta.env` avec fallbacks appropriĂ©s + +### 🟡 MEDIUM (QualitĂ©/Performance) + +6. **480+ utilisations de `any`** + - Impact : Perte de sĂ©curitĂ© de type, bugs potentiels + - Fix : Typage progressif, interfaces strictes + +7. **122+ console.log/error/warn** + - Impact : Pollution des logs de production, fuite d'informations + - Fix : Utiliser un logger conditionnel basĂ© sur `import.meta.env.DEV` + +8. **11 TODO/FIXME/HACK** + - Impact : Dette technique, fonctionnalitĂ©s incomplĂštes + - Fix : Prioriser et rĂ©soudre ou documenter + +### 🟱 LOW (CosmĂ©tique/Maintenance) + +9. **Code temporaire** (`LibraryManager.tsx:112`) + - Impact : Code mort, confusion + - Fix : Supprimer ou implĂ©menter la fonctionnalitĂ© + +10. **Tests manquants** (Plusieurs composants) + - Impact : Risque de rĂ©gression + - Fix : Augmenter la couverture de tests + +--- + +## 🎯 Plan d'Action RecommandĂ© + +### Phase 1 : SĂ©curitĂ© Critique (1-2 jours) +- [ ] Corriger CSP (supprimer unsafe-inline/unsafe-eval) +- [ ] Migrer tokens vers httpOnly cookies +- [ ] Audit complet de `dangerouslySetInnerHTML` + +### Phase 2 : QualitĂ© Code (2-3 jours) +- [ ] Remplacer tous les `console.*` par un logger conditionnel +- [ ] Corriger hardcoding localhost +- [ ] AmĂ©liorer gestion d'erreur (toasts partout) + +### Phase 3 : Dette Technique (1-2 jours) +- [ ] RĂ©soudre/prioriser les TODO +- [ ] RĂ©duire les `any` (typage progressif) +- [ ] Nettoyer le code temporaire + +--- + +## 📈 MĂ©triques + +- **Fichiers analysĂ©s** : ~363 fichiers +- **ProblĂšmes identifiĂ©s** : 50+ (voir JSON dĂ©taillĂ©) +- **Bloquants production** : 2 (CRITICAL) +- **Risques sĂ©curitĂ©** : 3 (HIGH) +- **ProblĂšmes qualitĂ©** : 8 (MEDIUM) +- **AmĂ©liorations** : 10+ (LOW) + +--- + +**Prochaine Ă©tape** : Consulter le JSON dĂ©taillĂ© (`AUDIT_ISSUES.json`) pour la liste complĂšte des problĂšmes avec localisation exacte. diff --git a/apps/web/DIAGNOSTIC_REPORT.md b/apps/web/DIAGNOSTIC_REPORT.md new file mode 100644 index 000000000..c4c7eeaf6 --- /dev/null +++ b/apps/web/DIAGNOSTIC_REPORT.md @@ -0,0 +1,272 @@ +# Rapport de Diagnostic - CompatibilitĂ© Full Stack + +**Date** : 2025-01-27 +**Test** : Login Flow - Full Stack Compatibility Diagnostic +**Environnement** : Frontend (localhost:3000) + Backend (localhost:8080) + +--- + +## 1. État Visuel & Navigation + +### ✅ Navigation de Base +- **Page accessible** : ✅ Oui (`http://localhost:3000/login`) +- **Titre de la page** : ✅ "Veza - Plateforme de streaming musical" +- **URL finale** : `http://localhost:3000/login` + +### ❌ Formulaire de Login +- **Formulaire visible** : ❌ **NON** +- **Inputs email/password** : ❌ **0 inputs dĂ©tectĂ©s sur la page** +- **Boutons** : ❌ **0 boutons dĂ©tectĂ©s sur la page** +- **Contenu HTML** : La page contient le mot "form" mais aucun Ă©lĂ©ment de formulaire n'est rendu + +**Diagnostic** : Le formulaire React ne se charge pas. La page est essentiellement vide cĂŽtĂ© contenu interactif. + +--- + +## 2. Analyse "Sous le capot" (Backend Compatibility) + +### 🔮 Erreurs RĂ©seau + +**Erreur 500 dĂ©tectĂ©e** lors du chargement des ressources Vite : + +1. **`Failed to load resource: the server responded with a status of 500 (Internal Server Error)`** + - **Impact** : Les scripts Vite ne peuvent pas se charger, empĂȘchant React de s'initialiser + - **Scripts affectĂ©s** : + - `/@vite/client` - Client Vite pour le HMR + - `/src/main.tsx?t=...` - Point d'entrĂ©e React + +**Diagnostic** : Le serveur Vite retourne une erreur 500, probablement due Ă  : +- Une erreur de compilation TypeScript/JavaScript +- Un problĂšme d'import de module +- Une erreur dans le code React qui empĂȘche le build + +#### RequĂȘtes Attendues (si le formulaire fonctionnait) : +1. `POST http://localhost:8080/api/v1/auth/login` + - **Payload attendu** : `{ email: string, password: string, remember_me?: boolean }` + - **Format attendu** : `{ success: true, data: { access_token, refresh_token, expires_in, token_type, user } }` + +#### Erreurs Potentielles Ă  Surveiller : +- **401 Unauthorized** : Identifiants invalides +- **400 Bad Request** : Format de payload incorrect +- **404 Not Found** : Endpoint `/auth/login` introuvable +- **500 Internal Server Error** : Erreur serveur +- **CORS** : Blocage cross-origin + +### 🔮 Erreurs Console + +**Erreurs capturĂ©es** : + +1. **`Failed to load resource: the server responded with a status of 500 (Internal Server Error)`** + - **Type** : Error + - **Cause** : Le serveur Vite ne peut pas servir les modules JavaScript/TypeScript + - **Impact** : React ne peut pas s'initialiser, le formulaire ne se rend pas + +**Messages console capturĂ©s** : +- `[debug] [vite] connecting...` +- `[debug] [vite] connected.` +- `[error] Failed to load resource: the server responded with a status of 500 (Internal Server Error)` + +**Diagnostic** : Le problĂšme vient du serveur de dĂ©veloppement Vite qui retourne une erreur 500 lors du chargement des modules. Cela empĂȘche complĂštement le rendu de l'application React. + +**Erreurs TypeScript DĂ©tectĂ©es** : +- Plusieurs erreurs TypeScript dans des composants non liĂ©s Ă  l'auth (player, search, forms) +- Ces erreurs peuvent empĂȘcher Vite de compiler correctement +- **Note** : Ces erreurs existaient probablement avant le refactoring de l'auth + +### 🟠 CORS + +**Aucune erreur CORS dĂ©tectĂ©e** car aucune requĂȘte n'a Ă©tĂ© faite. + +**Configuration Backend Requise** : +```go +CORS_ALLOWED_ORIGINS=http://localhost:3000 +``` + +### 🟱 Token + +**LocalStorage aprĂšs test** : Non vĂ©rifiĂ© (le formulaire ne s'est pas chargĂ©) + +**ClĂ©s attendues** : +- `access_token` ou `veza_access_token` +- `refresh_token` ou `veza_refresh_token` + +--- + +## 3. Verdict & Bloquants + +### 🔮 **BLOQUANT CRITIQUE #1 : Erreur 500 du Serveur Vite** + +**ProblĂšme** : Le serveur Vite retourne une erreur 500 lors du chargement des modules, empĂȘchant React de s'initialiser. + +**Cause IdentifiĂ©e** : +- **Erreur 500** sur `/@vite/client` et `/src/main.tsx` +- Les scripts ne peuvent pas se charger +- React ne peut pas s'initialiser +- Le formulaire ne se rend pas (0 inputs, 0 boutons) + +**Causes Possibles** : +1. **Erreur de compilation TypeScript** dans `src/main.tsx` ou un module importĂ© + - ✅ **ConfirmĂ©** : Plusieurs erreurs TypeScript dĂ©tectĂ©es (player, search, forms) + - Ces erreurs empĂȘchent Vite de compiler correctement +2. **Erreur d'import** - un module ne peut pas ĂȘtre rĂ©solu + - Exemples : `@/stores/player`, `@/hooks/use-toast`, `@/components/ui/scroll-area` +3. **Erreur de syntaxe** dans un fichier rĂ©cemment modifiĂ© +4. **ProblĂšme de configuration Vite** - alias ou plugins mal configurĂ©s + +**Actions ImmĂ©diates** : + +1. **VĂ©rifier les logs du serveur Vite** (PRIORITÉ ABSOLUE) : + ```bash + # ArrĂȘter le serveur actuel (Ctrl+C) + # Relancer avec logs dĂ©taillĂ©s + npm run dev + # Observer les erreurs de compilation TypeScript/JavaScript + ``` + +2. **VĂ©rifier les erreurs TypeScript** : + ```bash + npm run typecheck + # Corriger toutes les erreurs TypeScript + ``` + +3. **VĂ©rifier les erreurs dans la console du navigateur** : + - Ouvrir `http://localhost:3000/login` dans un navigateur + - Ouvrir DevTools (F12) + - VĂ©rifier l'onglet Console pour les erreurs dĂ©taillĂ©es + - VĂ©rifier l'onglet Network pour voir quelle requĂȘte retourne 500 + +4. **VĂ©rifier les imports rĂ©cents** : + ```bash + # VĂ©rifier que tous les imports dans les fichiers modifiĂ©s sont valides + grep -r "from.*@/stores/auth" src/ + grep -r "from.*@/services/api/auth" src/ + ``` + +5. **VĂ©rifier les erreurs de build** : + ```bash + npm run build + # Observer les erreurs de compilation + ``` + +### 🟡 **BLOQUANT MOYEN : Test Incomplet** + +**ProblĂšme** : Le test ne peut pas continuer car le formulaire ne se charge pas. + +**AmĂ©liorations NĂ©cessaires** : +1. Capturer les erreurs console dĂšs le chargement de la page +2. Prendre des captures d'Ă©cran automatiques +3. Logger le HTML complet de la page en cas d'Ă©chec +4. VĂ©rifier si le serveur backend rĂ©pond avant de tester le login + +--- + +## 4. Plan d'Action ImmĂ©diat + +### PrioritĂ© 1 : Diagnostiquer le ProblĂšme de Rendu (30 min) + +1. **VĂ©rifier le serveur frontend** : + ```bash + cd apps/web + npm run dev + # Ouvrir http://localhost:3000/login dans un navigateur + ``` + +2. **VĂ©rifier les erreurs console** : + - Ouvrir DevTools + - VĂ©rifier l'onglet Console + - VĂ©rifier l'onglet Network pour les erreurs 404/500 + +3. **VĂ©rifier le routing** : + ```bash + # VĂ©rifier que la route /login existe + grep -r "/login" src/router/ + ``` + +4. **VĂ©rifier les imports** : + ```bash + # VĂ©rifier que LoginForm est bien importĂ© dans LoginPage + grep -r "LoginForm" src/pages/LoginPage.tsx + ``` + +### PrioritĂ© 2 : Une fois le Formulaire Visible (15 min) + +1. **Relancer le test de diagnostic** +2. **VĂ©rifier les requĂȘtes rĂ©seau** vers `localhost:8080` +3. **VĂ©rifier le format du payload** envoyĂ© +4. **VĂ©rifier le format de la rĂ©ponse** reçue +5. **VĂ©rifier le stockage du token** dans localStorage + +### PrioritĂ© 3 : Tests d'IntĂ©gration Complets (30 min) + +1. **Test avec identifiants valides** (si backend disponible) +2. **Test avec identifiants invalides** (vĂ©rifier les messages d'erreur) +3. **Test de redirection** aprĂšs login rĂ©ussi +4. **Test de persistance** du token (refresh de page) + +--- + +## 5. Commandes de Diagnostic + +### VĂ©rifier le Serveur Frontend +```bash +curl http://localhost:3000/login +``` + +### VĂ©rifier le Serveur Backend +```bash +curl http://localhost:8080/api/v1/health +``` + +### Lancer le Test de Diagnostic +```bash +cd apps/web +npx playwright test e2e/diagnostic.spec.ts --reporter=list +``` + +### VĂ©rifier les Erreurs TypeScript +```bash +cd apps/web +npm run typecheck +``` + +### VĂ©rifier les Erreurs de Build +```bash +cd apps/web +npm run build +``` + +--- + +## 6. Notes Techniques + +### Configuration Playwright +- **Base URL** : `http://localhost:3000` (configurĂ© dans `playwright.config.ts`) +- **Timeout** : 30 secondes pour la navigation +- **Browser** : Chromium Headless Shell + +### Variables d'Environnement +- `VITE_API_URL` : URL du backend (dĂ©faut: `http://localhost:8080/api/v1`) +- `TEST_EMAIL` : Email de test (dĂ©faut: `user@example.com`) +- `TEST_PASSWORD` : Mot de passe de test (dĂ©faut: `password123`) + +--- + +## Conclusion + +**État Actuel** : 🔮 **BLOQUÉ** - Erreur 500 du serveur Vite empĂȘchant le chargement de React. + +**Cause Racine IdentifiĂ©e** : Le serveur Vite retourne une erreur 500 lors du chargement des modules (`/@vite/client` et `/src/main.tsx`), probablement due Ă  une erreur de compilation TypeScript ou un problĂšme d'import. + +**Prochaine Étape** : +1. **URGENT** : VĂ©rifier les logs du serveur Vite (`npm run dev`) pour identifier l'erreur exacte +2. Corriger l'erreur de compilation +3. Relancer le serveur +4. Relancer le test de diagnostic + +**Une fois le formulaire visible** : Relancer le test de diagnostic pour vĂ©rifier la communication avec le backend et le format des requĂȘtes/rĂ©ponses. + +--- + +**GĂ©nĂ©rĂ© par** : Script de diagnostic Playwright (`e2e/diagnostic.spec.ts`) +**Date du test** : 2025-01-27 + diff --git a/apps/web/RUNTIME_AUDIT_REPORT.md b/apps/web/RUNTIME_AUDIT_REPORT.md new file mode 100644 index 000000000..6763d96a1 --- /dev/null +++ b/apps/web/RUNTIME_AUDIT_REPORT.md @@ -0,0 +1,69 @@ +# Runtime Audit Report + +**Generated:** 2025-12-17T12:09:00.157Z + +--- + +## État Global + +**Status:** ❌ UNSTABLE +**Login Success:** ✅ Yes + +## Parcours Utilisateur + +| Page | Loaded | Has Content | Load Time (ms) | +|------|--------|-------------|----------------| +| /dashboard | ✅ | ✅ | 15ms | +| /profile | ❌ | ❌ | N/A | +| /settings | ❌ | ❌ | N/A | +| /library | ❌ | ❌ | N/A | + +## RĂ©sumĂ© des Erreurs + +**Total Issues:** 3 + +### Par SĂ©vĂ©ritĂ© + +- **CRITICAL:** 3 +- **HIGH:** 0 +- **MEDIUM:** 0 +- **LOW:** 0 + +### Par CatĂ©gorie + +- **NETWORK:** 0 +- **CONSOLE:** 0 +- **NAVIGATION:** 3 +- **UX:** 0 + +## Erreurs Navigation + +### RUN-001 - CRITICAL + +- **Location:** /profile +- **Message:** Failed to navigate to /profile +- **Details:** page.waitForURL: Timeout 10000ms exceeded. +=========================== logs =========================== +waiting for navigation until "domcontentloaded" +============================================================ +- **Reproduction:** Navigate to /profile after login + +### RUN-002 - CRITICAL + +- **Location:** /settings +- **Message:** Failed to navigate to /settings +- **Details:** page.waitForURL: Timeout 10000ms exceeded. +=========================== logs =========================== +waiting for navigation until "domcontentloaded" +============================================================ +- **Reproduction:** Navigate to /settings after login + +### RUN-003 - CRITICAL + +- **Location:** /library +- **Message:** Failed to navigate to /library +- **Details:** page.waitForURL: Timeout 10000ms exceeded. +=========================== logs =========================== +waiting for navigation until "domcontentloaded" +============================================================ +- **Reproduction:** Navigate to /library after login diff --git a/apps/web/RUNTIME_ISSUES.json b/apps/web/RUNTIME_ISSUES.json new file mode 100644 index 000000000..2e3fa86a4 --- /dev/null +++ b/apps/web/RUNTIME_ISSUES.json @@ -0,0 +1,32 @@ +[ + { + "category": "NAVIGATION", + "severity": "CRITICAL", + "location": "/profile", + "message": "Failed to navigate to /profile", + "details": "page.waitForURL: Timeout 10000ms exceeded.\n=========================== logs ===========================\nwaiting for navigation until \"domcontentloaded\"\n============================================================", + "reproduction_steps": "Navigate to /profile after login", + "id": "RUN-001", + "timestamp": "2025-12-17T12:08:36.888Z" + }, + { + "category": "NAVIGATION", + "severity": "CRITICAL", + "location": "/settings", + "message": "Failed to navigate to /settings", + "details": "page.waitForURL: Timeout 10000ms exceeded.\n=========================== logs ===========================\nwaiting for navigation until \"domcontentloaded\"\n============================================================", + "reproduction_steps": "Navigate to /settings after login", + "id": "RUN-002", + "timestamp": "2025-12-17T12:08:48.535Z" + }, + { + "category": "NAVIGATION", + "severity": "CRITICAL", + "location": "/library", + "message": "Failed to navigate to /library", + "details": "page.waitForURL: Timeout 10000ms exceeded.\n=========================== logs ===========================\nwaiting for navigation until \"domcontentloaded\"\n============================================================", + "reproduction_steps": "Navigate to /library after login", + "id": "RUN-003", + "timestamp": "2025-12-17T12:09:00.142Z" + } +] \ No newline at end of file diff --git a/apps/web/e2e-results.json b/apps/web/e2e-results.json new file mode 100644 index 000000000..fa6e0ba9d --- /dev/null +++ b/apps/web/e2e-results.json @@ -0,0 +1,207 @@ +{ + "config": { + "configFile": "/home/senke/git/talas/veza/apps/web/playwright.config.ts", + "rootDir": "/home/senke/git/talas/veza/apps/web/e2e", + "forbidOnly": false, + "fullyParallel": true, + "globalSetup": null, + "globalTeardown": null, + "globalTimeout": 0, + "grep": {}, + "grepInvert": null, + "maxFailures": 0, + "metadata": { + "actualWorkers": 1 + }, + "preserveOutput": "always", + "reporter": [ + [ + "html", + null + ], + [ + "json", + { + "outputFile": "e2e-results.json" + } + ] + ], + "reportSlowTests": { + "max": 5, + "threshold": 300000 + }, + "quiet": false, + "projects": [ + { + "outputDir": "/home/senke/git/talas/veza/apps/web/test-results", + "repeatEach": 1, + "retries": 0, + "metadata": { + "actualWorkers": 1 + }, + "id": "chromium", + "name": "chromium", + "testDir": "/home/senke/git/talas/veza/apps/web/e2e", + "testIgnore": [], + "testMatch": [ + "**/*.@(spec|test).?(c|m)[jt]s?(x)" + ], + "timeout": 30000 + } + ], + "shard": null, + "tags": [], + "updateSnapshots": "missing", + "updateSourceMethod": "patch", + "version": "1.57.0", + "workers": 6, + "webServer": { + "command": "npm run dev", + "url": "http://localhost:3000", + "reuseExistingServer": true, + "timeout": 120000 + } + }, + "suites": [ + { + "title": "deep_audit.spec.ts", + "file": "deep_audit.spec.ts", + "column": 0, + "line": 0, + "specs": [], + "suites": [ + { + "title": "Deep E2E Runtime Audit", + "file": "deep_audit.spec.ts", + "line": 175, + "column": 6, + "specs": [ + { + "title": "Complete User Journey - Runtime Audit", + "ok": true, + "tags": [], + "tests": [ + { + "timeout": 60000, + "annotations": [], + "expectedStatus": "passed", + "projectId": "chromium", + "projectName": "chromium", + "results": [ + { + "workerIndex": 0, + "parallelIndex": 0, + "status": "passed", + "duration": 38597, + "errors": [], + "stdout": [ + { + "text": "🔍 [AUDIT] Starting comprehensive E2E audit...\n" + }, + { + "text": "🔍 [AUDIT] Step 1: Navigating to login...\n" + }, + { + "text": "🔍 [AUDIT] Step 2: Submitting login form...\n" + }, + { + "text": "✅ [AUDIT] Login successful, redirected to: http://localhost:3000/dashboard\n" + }, + { + "text": "🔍 [AUDIT] Step 3: Testing page navigation and lazy loading...\n" + }, + { + "text": " → Checking /dashboard...\n" + }, + { + "text": " → Navigating to /profile...\n" + }, + { + "text": " → Navigating to /settings...\n" + }, + { + "text": " → Navigating to /library...\n" + }, + { + "text": "\n📊 [AUDIT] === AUDIT SUMMARY ===\n" + }, + { + "text": "Global Status: UNSTABLE\n" + }, + { + "text": "Login Success: true\n" + }, + { + "text": "Pages Checked: 4\n" + }, + { + "text": "Total Issues: 3\n" + }, + { + "text": " - Critical: 3\n" + }, + { + "text": " - High: 0\n" + }, + { + "text": " - Medium: 0\n" + }, + { + "text": " - Low: 0\n" + }, + { + "text": "By Category:\n" + }, + { + "text": " - NETWORK: 0\n" + }, + { + "text": " - CONSOLE: 0\n" + }, + { + "text": " - NAVIGATION: 3\n" + }, + { + "text": " - UX: 0\n" + }, + { + "text": "📄 [AUDIT] JSON report written to: /home/senke/git/talas/veza/apps/web/RUNTIME_ISSUES.json\n" + }, + { + "text": "📄 [AUDIT] Markdown report written to: /home/senke/git/talas/veza/apps/web/RUNTIME_AUDIT_REPORT.md\n" + } + ], + "stderr": [ + { + "text": "❌ [AUDIT] Application is UNSTABLE\n" + } + ], + "retry": 0, + "startTime": "2025-12-17T12:08:22.106Z", + "annotations": [], + "attachments": [] + } + ], + "status": "expected" + } + ], + "id": "31d47f59884aa29aa4c6-10f97fe6bc18ecc2bb0a", + "file": "deep_audit.spec.ts", + "line": 202, + "column": 3 + } + ] + } + ] + } + ], + "errors": [], + "stats": { + "startTime": "2025-12-17T12:08:21.296Z", + "duration": 39518.613, + "expected": 1, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + } +} \ No newline at end of file diff --git a/apps/web/e2e/deep_audit.spec.ts b/apps/web/e2e/deep_audit.spec.ts new file mode 100644 index 000000000..edd77cc8d --- /dev/null +++ b/apps/web/e2e/deep_audit.spec.ts @@ -0,0 +1,710 @@ +import { test, expect, type Page } from '@playwright/test'; +import { writeFileSync } from 'fs'; +import { join } from 'path'; + +/** + * Deep E2E Audit - Runtime Stability Check + * + * Ce test effectue un parcours utilisateur complet et capture toutes les erreurs + * Runtime, RĂ©seau, et d'IntĂ©gration pour valider la stabilitĂ© aprĂšs les corrections + * de lazy loading. + */ + +// Configuration +const FRONTEND_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000'; +const TEST_EMAIL = process.env.TEST_EMAIL || 'user@example.com'; +const TEST_PASSWORD = process.env.TEST_PASSWORD || 'password123'; + +// Types pour le rapport +interface RuntimeIssue { + id: string; + category: 'NETWORK' | 'CONSOLE' | 'NAVIGATION' | 'UX'; + severity: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'; + location: string; + message: string; + details?: string; + reproduction_steps: string; + timestamp?: string; +} + +interface PageCheckResult { + path: string; + loaded: boolean; + hasContent: boolean; + errors: RuntimeIssue[]; + loadTime?: number; +} + +interface AuditReport { + globalStatus: 'STABLE' | 'UNSTABLE'; + loginSuccess: boolean; + pages: PageCheckResult[]; + allIssues: RuntimeIssue[]; + summary: { + totalIssues: number; + critical: number; + high: number; + medium: number; + low: number; + byCategory: { + NETWORK: number; + CONSOLE: number; + NAVIGATION: number; + UX: number; + }; + }; +} + +// Collecteurs globaux +let allIssues: RuntimeIssue[] = []; +let issueCounter = 1; + +function generateIssueId(): string { + return `RUN-${String(issueCounter++).padStart(3, '0')}`; +} + +function addIssue(issue: Omit): void { + allIssues.push({ + ...issue, + id: generateIssueId(), + timestamp: new Date().toISOString(), + }); +} + +// Helper pour vĂ©rifier qu'une page a du contenu +async function checkPageHasContent(page: Page, selectors: string[]): Promise { + // VĂ©rifier d'abord que le body n'est pas vide + const bodyText = await page.locator('body').textContent().catch(() => ''); + if (!bodyText || bodyText.trim().length < 10) { + return false; + } + + // VĂ©rifier les sĂ©lecteurs spĂ©cifiques + for (const selector of selectors) { + try { + const count = await page.locator(selector).count(); + if (count > 0) { + const element = await page.locator(selector).first(); + if (await element.isVisible({ timeout: 2000 })) { + return true; + } + } + } catch { + continue; + } + } + + // Si aucun sĂ©lecteur spĂ©cifique n'est trouvĂ©, vĂ©rifier qu'il y a au moins du contenu dans main ou body + const mainContent = await page.locator('main, [role="main"], .main-content').first().textContent().catch(() => ''); + if (mainContent && mainContent.trim().length > 10) { + return true; + } + + return false; +} + +// Helper pour attendre qu'une page charge +async function waitForPageLoad( + page: Page, + expectedPath: string, + contentSelectors: string[], + timeout = 10000 +): Promise { + const startTime = Date.now(); + const result: PageCheckResult = { + path: expectedPath, + loaded: false, + hasContent: false, + errors: [], + }; + + try { + // VĂ©rifier d'abord si on est dĂ©jĂ  sur la bonne page + const currentPath = new URL(page.url()).pathname; + if (currentPath !== expectedPath) { + // Attendre la navigation seulement si on n'est pas dĂ©jĂ  sur la bonne page + await page.waitForURL( + (url) => url.pathname === expectedPath, + { timeout, waitUntil: 'domcontentloaded' } + ); + } + result.loaded = true; + + // Attendre que le rĂ©seau soit idle + await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => { + addIssue({ + category: 'NAVIGATION', + severity: 'MEDIUM', + location: expectedPath, + message: 'Page took too long to reach networkidle state', + details: `Timeout after 5s waiting for networkidle`, + reproduction_steps: `Navigate to ${expectedPath}`, + }); + }); + + // VĂ©rifier qu'il y a du contenu + result.hasContent = await checkPageHasContent(page, contentSelectors); + + if (!result.hasContent) { + addIssue({ + category: 'UX', + severity: 'HIGH', + location: expectedPath, + message: 'Page appears to be blank or empty', + details: `None of the expected selectors found: ${contentSelectors.join(', ')}`, + reproduction_steps: `Navigate to ${expectedPath} after login`, + }); + } + + result.loadTime = Date.now() - startTime; + } catch (error) { + result.loaded = false; + addIssue({ + category: 'NAVIGATION', + severity: 'CRITICAL', + location: expectedPath, + message: `Failed to navigate to ${expectedPath}`, + details: error instanceof Error ? error.message : String(error), + reproduction_steps: `Navigate to ${expectedPath} after login`, + }); + } + + return result; +} + +test.describe('Deep E2E Runtime Audit', () => { + let report: AuditReport; + + test.beforeEach(() => { + allIssues = []; + issueCounter = 1; + report = { + globalStatus: 'STABLE', + loginSuccess: false, + pages: [], + allIssues: [], + summary: { + totalIssues: 0, + critical: 0, + high: 0, + medium: 0, + low: 0, + byCategory: { + NETWORK: 0, + CONSOLE: 0, + NAVIGATION: 0, + UX: 0, + }, + }, + }; + }); + + test('Complete User Journey - Runtime Audit', async ({ page, context }) => { + test.setTimeout(60000); // 60 secondes pour le test complet + console.log('🔍 [AUDIT] Starting comprehensive E2E audit...'); + + // ============================================ + // PHASE 1: Setup Error Listeners + // ============================================ + + // Console errors & warnings + page.on('console', (msg) => { + const type = msg.type(); + const text = msg.text(); + const location = page.url(); + + if (type === 'error') { + addIssue({ + category: 'CONSOLE', + severity: 'HIGH', + location, + message: text, + details: `Console error: ${text}`, + reproduction_steps: `Navigate to ${location}`, + }); + console.log(`🔮 [CONSOLE ERROR] ${text}`); + } else if (type === 'warning') { + // MĂȘme les warnings sont capturĂ©s (comme demandĂ©) + addIssue({ + category: 'CONSOLE', + severity: 'MEDIUM', + location, + message: text, + details: `Console warning: ${text}`, + reproduction_steps: `Navigate to ${location}`, + }); + console.log(`🟡 [CONSOLE WARNING] ${text}`); + } + }); + + // Page errors (uncaught exceptions) + page.on('pageerror', (error) => { + addIssue({ + category: 'CONSOLE', + severity: 'CRITICAL', + location: page.url(), + message: error.message, + details: error.stack, + reproduction_steps: `Navigate to ${page.url()}`, + }); + console.log(`🔮 [PAGE ERROR] ${error.message}`); + }); + + // Network errors (4xx, 5xx) + page.on('response', async (response) => { + const status = response.status(); + const url = response.url(); + const method = response.request().method(); + + if (status >= 400) { + const severity = status >= 500 ? 'CRITICAL' : status >= 400 ? 'HIGH' : 'MEDIUM'; + + // Essayer de rĂ©cupĂ©rer le body de l'erreur pour plus de dĂ©tails + let errorDetails = `Server responded with status ${status}`; + try { + const responseBody = await response.text().catch(() => ''); + if (responseBody) { + try { + const parsed = JSON.parse(responseBody); + if (parsed && parsed.error) { + errorDetails = `${errorDetails}. Error: ${parsed.error}`; + } else if (parsed && parsed.message) { + errorDetails = `${errorDetails}. Message: ${parsed.message}`; + } + } catch { + // Si ce n'est pas du JSON, prendre un extrait du texte + if (responseBody.length < 200) { + errorDetails = `${errorDetails}. Response: ${responseBody.substring(0, 200)}`; + } + } + } + } catch { + // Ignore si on ne peut pas parser la rĂ©ponse + } + + addIssue({ + category: 'NETWORK', + severity, + location: page.url(), + message: `HTTP ${status} - ${method} ${url}`, + details: errorDetails, + reproduction_steps: `Navigate to ${page.url()}`, + }); + console.log(`🔮 [NETWORK ERROR] ${method} ${url} -> ${status}`); + } + }); + + // Failed requests (network failures) + page.on('requestfailed', (request) => { + const failure = request.failure(); + if (failure) { + const url = request.url(); + const method = request.method(); + + // Ne pas reporter les erreurs de favicon ou de ressources statiques non critiques + if (url.includes('favicon') || url.includes('.ico') || url.includes('chrome-extension')) { + return; + } + + addIssue({ + category: 'NETWORK', + severity: 'CRITICAL', + location: page.url(), + message: `Request failed: ${method} ${url}`, + details: failure.errorText || 'Network error', + reproduction_steps: `Navigate to ${page.url()}`, + }); + console.log(`🔮 [REQUEST FAILED] ${method} ${url}: ${failure.errorText}`); + } + }); + + // ============================================ + // PHASE 2: Login Flow + // ============================================ + + console.log('🔍 [AUDIT] Step 1: Navigating to login...'); + await page.goto(`${FRONTEND_URL}/login`, { + waitUntil: 'domcontentloaded', + timeout: 30000, + }); + + await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => { + console.warn('⚠ [AUDIT] Timeout on networkidle for login page'); + }); + + // Attendre que le formulaire soit chargĂ© + await page.waitForSelector('input[type="email"], input[name="email"]', { + timeout: 10000, + }).catch(() => { + addIssue({ + category: 'UX', + severity: 'CRITICAL', + location: '/login', + message: 'Login form not found', + details: 'Email input field not visible', + reproduction_steps: 'Navigate to /login', + }); + }); + + // Remplir le formulaire + const emailInput = page.locator('input[type="email"], input[name="email"]').first(); + const passwordInput = page.locator('input[type="password"]').first(); + const submitButton = page.locator('button[type="submit"], button:has-text("connecter"), button:has-text("login"), button:has-text("Se connecter")').first(); + + await emailInput.fill(TEST_EMAIL); + await passwordInput.fill(TEST_PASSWORD); + + console.log('🔍 [AUDIT] Step 2: Submitting login form...'); + + // Attendre la navigation aprĂšs login + await submitButton.click(); + + // Attendre soit la navigation, soit un message d'erreur + try { + await page.waitForURL( + (url) => url.pathname === '/dashboard' || url.pathname === '/', + { timeout: 15000 } + ); + report.loginSuccess = true; + console.log('✅ [AUDIT] Login successful, redirected to:', page.url()); + } catch (error) { + // VĂ©rifier si on est toujours sur /login ou si on a une erreur + const currentUrl = page.url(); + const currentPath = new URL(currentUrl).pathname; + + if (currentPath === '/login') { + report.loginSuccess = false; + addIssue({ + category: 'NAVIGATION', + severity: 'CRITICAL', + location: '/login', + message: 'Login failed or did not redirect', + details: `Still on ${currentUrl} after login attempt. Check for error messages or network failures.`, + reproduction_steps: `Login with ${TEST_EMAIL}`, + }); + console.error('❌ [AUDIT] Login failed or did not redirect'); + + // Si le login Ă©choue, on gĂ©nĂšre quand mĂȘme le rapport avec les erreurs capturĂ©es + report.allIssues = allIssues; + report.summary.totalIssues = allIssues.length; + report.summary.critical = allIssues.filter((i) => i.severity === 'CRITICAL').length; + report.summary.high = allIssues.filter((i) => i.severity === 'HIGH').length; + report.summary.medium = allIssues.filter((i) => i.severity === 'MEDIUM').length; + report.summary.low = allIssues.filter((i) => i.severity === 'LOW').length; + report.summary.byCategory.NETWORK = allIssues.filter((i) => i.category === 'NETWORK').length; + report.summary.byCategory.CONSOLE = allIssues.filter((i) => i.category === 'CONSOLE').length; + report.summary.byCategory.NAVIGATION = allIssues.filter((i) => i.category === 'NAVIGATION').length; + report.summary.byCategory.UX = allIssues.filter((i) => i.category === 'UX').length; + report.globalStatus = 'UNSTABLE'; + + // Sauvegarder le rapport mĂȘme en cas d'Ă©chec + await page.evaluate((report) => { + (window as any).__auditReport = report; + }, report); + + return; + } else { + // On a naviguĂ© ailleurs (peut-ĂȘtre une page d'erreur ou autre) + report.loginSuccess = false; + addIssue({ + category: 'NAVIGATION', + severity: 'HIGH', + location: currentPath, + message: 'Login redirected to unexpected page', + details: `Expected /dashboard but got ${currentUrl}`, + reproduction_steps: `Login with ${TEST_EMAIL}`, + }); + console.warn('⚠ [AUDIT] Login redirected to unexpected page:', currentUrl); + } + } + + // Attendre un peu pour que l'app se stabilise + await page.waitForTimeout(2000); + + // ============================================ + // PHASE 3: Navigation & Lazy Loading Check + // ============================================ + + console.log('🔍 [AUDIT] Step 3: Testing page navigation and lazy loading...'); + + // Dashboard (dĂ©jĂ  chargĂ©) + console.log(' → Checking /dashboard...'); + // S'assurer qu'on est bien sur /dashboard + if (new URL(page.url()).pathname !== '/dashboard') { + await page.goto(`${FRONTEND_URL}/dashboard`, { waitUntil: 'domcontentloaded' }); + } + const dashboardCheck = await waitForPageLoad( + page, + '/dashboard', + ['[data-testid="dashboard"]', 'h1', 'main', '.container', 'nav', 'aside'], + ); + report.pages.push(dashboardCheck); + + // Profile page + console.log(' → Navigating to /profile...'); + await page.goto(`${FRONTEND_URL}/profile`, { waitUntil: 'domcontentloaded', timeout: 15000 }); + await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {}); + await page.waitForTimeout(1000); // Attendre que le lazy loading se stabilise + const profileCheck = await waitForPageLoad( + page, + '/profile', + ['h1', 'main', '.container', '[data-testid="profile"]', 'form', 'button'], + ); + report.pages.push(profileCheck); + + // Settings page + console.log(' → Navigating to /settings...'); + await page.goto(`${FRONTEND_URL}/settings`, { waitUntil: 'domcontentloaded', timeout: 15000 }); + await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {}); + await page.waitForTimeout(1000); // Attendre que le lazy loading se stabilise + const settingsCheck = await waitForPageLoad( + page, + '/settings', + ['h1:has-text("ParamĂštres"), h1:has-text("Settings"), h1', 'main', '.container', 'form', 'button'], + ); + report.pages.push(settingsCheck); + + // Library page + console.log(' → Navigating to /library...'); + await page.goto(`${FRONTEND_URL}/library`, { waitUntil: 'domcontentloaded', timeout: 15000 }); + await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {}); + await page.waitForTimeout(1000); // Attendre que le lazy loading se stabilise + const libraryCheck = await waitForPageLoad( + page, + '/library', + ['h1', 'main', '.container', '[data-testid="library"]', 'button', 'div'], + ); + report.pages.push(libraryCheck); + + // ============================================ + // PHASE 4: Generate Report + // ============================================ + + report.allIssues = allIssues; + + // Calculer le rĂ©sumĂ© + report.summary.totalIssues = allIssues.length; + report.summary.critical = allIssues.filter((i) => i.severity === 'CRITICAL').length; + report.summary.high = allIssues.filter((i) => i.severity === 'HIGH').length; + report.summary.medium = allIssues.filter((i) => i.severity === 'MEDIUM').length; + report.summary.low = allIssues.filter((i) => i.severity === 'LOW').length; + + report.summary.byCategory.NETWORK = allIssues.filter((i) => i.category === 'NETWORK').length; + report.summary.byCategory.CONSOLE = allIssues.filter((i) => i.category === 'CONSOLE').length; + report.summary.byCategory.NAVIGATION = allIssues.filter((i) => i.category === 'NAVIGATION').length; + report.summary.byCategory.UX = allIssues.filter((i) => i.category === 'UX').length; + + // DĂ©terminer le statut global + if ( + report.summary.critical > 0 || + !report.loginSuccess || + report.pages.some((p) => !p.loaded || !p.hasContent) + ) { + report.globalStatus = 'UNSTABLE'; + } + + // Afficher le rĂ©sumĂ© dans la console + console.log('\n📊 [AUDIT] === AUDIT SUMMARY ==='); + console.log(`Global Status: ${report.globalStatus}`); + console.log(`Login Success: ${report.loginSuccess}`); + console.log(`Pages Checked: ${report.pages.length}`); + console.log(`Total Issues: ${report.summary.totalIssues}`); + console.log(` - Critical: ${report.summary.critical}`); + console.log(` - High: ${report.summary.high}`); + console.log(` - Medium: ${report.summary.medium}`); + console.log(` - Low: ${report.summary.low}`); + console.log(`By Category:`); + console.log(` - NETWORK: ${report.summary.byCategory.NETWORK}`); + console.log(` - CONSOLE: ${report.summary.byCategory.CONSOLE}`); + console.log(` - NAVIGATION: ${report.summary.byCategory.NAVIGATION}`); + console.log(` - UX: ${report.summary.byCategory.UX}`); + + // Sauvegarder le rapport dans la page pour rĂ©cupĂ©ration + await page.evaluate((report) => { + (window as any).__auditReport = report; + }, report); + + // Assertions finales (ne pas faire Ă©chouer le test, juste logger) + if (report.globalStatus === 'UNSTABLE') { + console.error('❌ [AUDIT] Application is UNSTABLE'); + } else { + console.log('✅ [AUDIT] Application appears STABLE'); + } + }); + + test.afterEach(async ({ page }) => { + // RĂ©cupĂ©rer le rapport depuis la page + const savedReport = await page + .evaluate(() => { + return (window as any).__auditReport; + }) + .catch(() => null); + + if (savedReport) { + report = savedReport; + } + + // Écrire les rapports dans des fichiers + // Utiliser process.cwd() car __dirname peut ne pas ĂȘtre disponible en ESM + const projectRoot = process.cwd(); + + // Rapport JSON + const jsonPath = join(projectRoot, 'RUNTIME_ISSUES.json'); + writeFileSync(jsonPath, JSON.stringify(report.allIssues, null, 2)); + console.log(`📄 [AUDIT] JSON report written to: ${jsonPath}`); + + // Rapport Markdown + const mdPath = join(projectRoot, 'RUNTIME_AUDIT_REPORT.md'); + const mdContent = generateMarkdownReport(report); + writeFileSync(mdPath, mdContent); + console.log(`📄 [AUDIT] Markdown report written to: ${mdPath}`); + }); +}); + +function generateMarkdownReport(report: AuditReport): string { + const lines: string[] = []; + + lines.push('# Runtime Audit Report'); + lines.push(''); + lines.push(`**Generated:** ${new Date().toISOString()}`); + lines.push(''); + lines.push('---'); + lines.push(''); + + // État Global + lines.push('## État Global'); + lines.push(''); + lines.push(`**Status:** ${report.globalStatus === 'STABLE' ? '✅ STABLE' : '❌ UNSTABLE'}`); + lines.push(`**Login Success:** ${report.loginSuccess ? '✅ Yes' : '❌ No'}`); + lines.push(''); + + // Parcours + lines.push('## Parcours Utilisateur'); + lines.push(''); + lines.push('| Page | Loaded | Has Content | Load Time (ms) |'); + lines.push('|------|--------|-------------|----------------|'); + for (const page of report.pages) { + const loaded = page.loaded ? '✅' : '❌'; + const content = page.hasContent ? '✅' : '❌'; + const loadTime = page.loadTime ? `${page.loadTime}ms` : 'N/A'; + lines.push(`| ${page.path} | ${loaded} | ${content} | ${loadTime} |`); + } + lines.push(''); + + // RĂ©sumĂ© des erreurs + lines.push('## RĂ©sumĂ© des Erreurs'); + lines.push(''); + lines.push(`**Total Issues:** ${report.summary.totalIssues}`); + lines.push(''); + lines.push('### Par SĂ©vĂ©ritĂ©'); + lines.push(''); + lines.push(`- **CRITICAL:** ${report.summary.critical}`); + lines.push(`- **HIGH:** ${report.summary.high}`); + lines.push(`- **MEDIUM:** ${report.summary.medium}`); + lines.push(`- **LOW:** ${report.summary.low}`); + lines.push(''); + + lines.push('### Par CatĂ©gorie'); + lines.push(''); + lines.push(`- **NETWORK:** ${report.summary.byCategory.NETWORK}`); + lines.push(`- **CONSOLE:** ${report.summary.byCategory.CONSOLE}`); + lines.push(`- **NAVIGATION:** ${report.summary.byCategory.NAVIGATION}`); + lines.push(`- **UX:** ${report.summary.byCategory.UX}`); + lines.push(''); + + // Erreurs Console + const consoleErrors = report.allIssues.filter((i) => i.category === 'CONSOLE'); + if (consoleErrors.length > 0) { + lines.push('## Erreurs Console'); + lines.push(''); + for (const error of consoleErrors) { + lines.push(`### ${error.id} - ${error.severity}`); + lines.push(''); + lines.push(`- **Location:** ${error.location}`); + lines.push(`- **Message:** ${error.message}`); + if (error.details) { + lines.push(`- **Details:** ${error.details}`); + } + lines.push(`- **Reproduction:** ${error.reproduction_steps}`); + lines.push(''); + } + } + + // Erreurs RĂ©seau + const networkErrors = report.allIssues.filter((i) => i.category === 'NETWORK'); + if (networkErrors.length > 0) { + lines.push('## Erreurs RĂ©seau'); + lines.push(''); + for (const error of networkErrors) { + lines.push(`### ${error.id} - ${error.severity}`); + lines.push(''); + lines.push(`- **Location:** ${error.location}`); + lines.push(`- **Message:** ${error.message}`); + if (error.details) { + lines.push(`- **Details:** ${error.details}`); + } + lines.push(`- **Reproduction:** ${error.reproduction_steps}`); + lines.push(''); + } + } + + // Erreurs Navigation + const navErrors = report.allIssues.filter((i) => i.category === 'NAVIGATION'); + if (navErrors.length > 0) { + lines.push('## Erreurs Navigation'); + lines.push(''); + for (const error of navErrors) { + lines.push(`### ${error.id} - ${error.severity}`); + lines.push(''); + lines.push(`- **Location:** ${error.location}`); + lines.push(`- **Message:** ${error.message}`); + if (error.details) { + lines.push(`- **Details:** ${error.details}`); + } + lines.push(`- **Reproduction:** ${error.reproduction_steps}`); + lines.push(''); + } + } + + // Erreurs UX + const uxErrors = report.allIssues.filter((i) => i.category === 'UX'); + if (uxErrors.length > 0) { + lines.push('## Erreurs UX'); + lines.push(''); + for (const error of uxErrors) { + lines.push(`### ${error.id} - ${error.severity}`); + lines.push(''); + lines.push(`- **Location:** ${error.location}`); + lines.push(`- **Message:** ${error.message}`); + if (error.details) { + lines.push(`- **Details:** ${error.details}`); + } + lines.push(`- **Reproduction:** ${error.reproduction_steps}`); + lines.push(''); + } + } + + if (report.allIssues.length === 0) { + lines.push('## ✅ Aucune Erreur DĂ©tectĂ©e'); + lines.push(''); + lines.push('L\'application semble stable. Aucune erreur runtime, rĂ©seau ou d\'intĂ©gration n\'a Ă©tĂ© dĂ©tectĂ©e.'); + } + + // Note sur les limitations du test + if (!report.loginSuccess) { + lines.push('---'); + lines.push(''); + lines.push('## ⚠ Note sur les Limitations du Test'); + lines.push(''); + lines.push('Le test n\'a pas pu continuer au-delĂ  de la page de login car le backend n\'Ă©tait pas accessible.'); + lines.push('Les pages protĂ©gĂ©es (Dashboard, Profile, Settings, Library) n\'ont donc pas pu ĂȘtre testĂ©es.'); + lines.push(''); + lines.push('**Pour un audit complet :**'); + lines.push('1. DĂ©marrer le backend API sur `http://localhost:8080`'); + lines.push('2. Configurer CORS pour autoriser les requĂȘtes depuis `http://localhost:3000`'); + lines.push('3. Relancer le test avec `npx playwright test e2e/deep_audit.spec.ts`'); + lines.push(''); + } + + return lines.join('\n'); +} diff --git a/apps/web/e2e/diagnostic-login-page.png b/apps/web/e2e/diagnostic-login-page.png new file mode 100644 index 000000000..bf218f30d Binary files /dev/null and b/apps/web/e2e/diagnostic-login-page.png differ diff --git a/apps/web/e2e/diagnostic.spec.ts b/apps/web/e2e/diagnostic.spec.ts new file mode 100644 index 000000000..ecba54785 --- /dev/null +++ b/apps/web/e2e/diagnostic.spec.ts @@ -0,0 +1,346 @@ +import { test, expect, type Page } from '@playwright/test'; + +/** + * Diagnostic Test - Full Stack Compatibility Check + * + * Ce test vĂ©rifie l'intĂ©gration Frontend-Backend aprĂšs le refactoring de l'authentification. + * Il capture toutes les erreurs rĂ©seau, console, CORS et vĂ©rifie le stockage des tokens. + */ + +// Configuration +const BASE_URL = process.env.VITE_API_URL || 'http://localhost:8080/api/v1'; +const FRONTEND_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000'; +const TEST_EMAIL = process.env.TEST_EMAIL || 'user@example.com'; +const TEST_PASSWORD = process.env.TEST_PASSWORD || 'password123'; + +// Collecteurs d'erreurs +interface DiagnosticReport { + networkErrors: Array<{ + url: string; + status: number; + method: string; + error: string; + }>; + consoleErrors: Array<{ + type: string; + message: string; + stack?: string; + }>; + corsErrors: Array<{ + url: string; + reason: string; + }>; + localStorage: Record; + navigationSuccess: boolean; + finalUrl: string; + formVisible: boolean; + errorMessage?: string; +} + +test.describe('Full Stack Compatibility Diagnostic', () => { + let report: DiagnosticReport; + + test.beforeEach(() => { + report = { + networkErrors: [], + consoleErrors: [], + corsErrors: [], + localStorage: {}, + navigationSuccess: false, + finalUrl: '', + formVisible: false, + }; + }); + + test('Login Flow - Complete Diagnostic', async ({ page, context }) => { + // Setup: Écouter les erreurs console AVANT toute navigation + const consoleMessages: Array<{ type: string; text: string }> = []; + page.on('console', (msg) => { + const type = msg.type(); + const text = msg.text(); + consoleMessages.push({ type, text }); + + if (type === 'error' || type === 'warning') { + report.consoleErrors.push({ + type, + message: text, + stack: msg.location()?.url, + }); + console.log(`🔮 [CONSOLE ${type.toUpperCase()}] ${text}`); + } + }); + + // Setup: Écouter les erreurs de page (uncaught exceptions) + page.on('pageerror', (error) => { + report.consoleErrors.push({ + type: 'pageerror', + message: error.message, + stack: error.stack, + }); + console.log(`🔮 [PAGE ERROR] ${error.message}`); + }); + + // Setup: Écouter les requĂȘtes rĂ©seau Ă©chouĂ©es + page.on('response', (response) => { + const status = response.status(); + const url = response.url(); + + // Capturer les erreurs 4xx et 5xx + if (status >= 400) { + report.networkErrors.push({ + url, + status, + method: response.request().method(), + error: `HTTP ${status}`, + }); + + // DĂ©tecter les erreurs CORS potentielles + if (status === 0 || url.includes('localhost:8080')) { + const headers = response.headers(); + if (!headers['access-control-allow-origin']) { + report.corsErrors.push({ + url, + reason: 'Missing CORS headers', + }); + } + } + } + }); + + // Setup: Écouter les requĂȘtes Ă©chouĂ©es (network errors) + page.on('requestfailed', (request) => { + const failure = request.failure(); + if (failure) { + report.networkErrors.push({ + url: request.url(), + status: 0, + method: request.method(), + error: failure.errorText || 'Network error', + }); + + // DĂ©tecter les erreurs CORS + if (failure.errorText?.includes('CORS') || failure.errorText?.includes('Access-Control')) { + report.corsErrors.push({ + url: request.url(), + reason: failure.errorText, + }); + } + } + }); + + // Étape 1: Aller sur la page de login + console.log('🔍 [DIAGNOSTIC] Navigation vers /login...'); + try { + await page.goto(`${FRONTEND_URL}/login`, { waitUntil: 'domcontentloaded', timeout: 30000 }); + } catch (error) { + console.error('❌ [DIAGNOSTIC] Erreur lors de la navigation:', error); + report.finalUrl = page.url(); + return; + } + + // Attendre que la page soit chargĂ©e + await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => { + console.warn('⚠ [DIAGNOSTIC] Timeout sur networkidle, continuation...'); + }); + + // Prendre une capture d'Ă©cran pour debug + await page.screenshot({ path: 'e2e/diagnostic-login-page.png', fullPage: true }); + + // Attendre que le formulaire soit chargĂ© (plusieurs stratĂ©gies) + try { + // Essayer d'attendre un Ă©lĂ©ment de formulaire + await page.waitForSelector('form, input[type="email"], input[type="password"]', { timeout: 10000 }); + } catch (e) { + console.warn('⚠ [DIAGNOSTIC] Timeout en attendant le formulaire'); + } + + // Attendre un peu pour que React hydrate + await page.waitForTimeout(3000); + + // VĂ©rifier que le formulaire est visible avec plusieurs sĂ©lecteurs possibles + const emailInput = page.locator('input[type="email"], input[name="email"], input[placeholder*="email" i]').first(); + const passwordInput = page.locator('input[type="password"], input[name="password"]').first(); + const submitButton = page.locator('button[type="submit"], button:has-text("connecter"), button:has-text("login"), button:has-text("Se connecter")').first(); + + // VĂ©rifier aussi avec des sĂ©lecteurs plus gĂ©nĂ©riques + const allInputs = await page.locator('input').count(); + const allButtons = await page.locator('button').count(); + console.log('📄 [DIAGNOSTIC] Nombre d\'inputs sur la page:', allInputs); + console.log('📄 [DIAGNOSTIC] Nombre de boutons sur la page:', allButtons); + + const emailVisible = await emailInput.isVisible().catch(() => false); + const passwordVisible = await passwordInput.isVisible().catch(() => false); + const submitVisible = await submitButton.isVisible().catch(() => false); + + report.formVisible = emailVisible && passwordVisible; + + // Logger le contenu de la page pour debug + const pageContent = await page.content(); + const hasForm = pageContent.includes('form') || pageContent.includes('email') || pageContent.includes('password'); + + console.log('📄 [DIAGNOSTIC] Page title:', await page.title()); + console.log('📄 [DIAGNOSTIC] URL actuelle:', page.url()); + console.log('📄 [DIAGNOSTIC] Email input visible:', emailVisible); + console.log('📄 [DIAGNOSTIC] Password input visible:', passwordVisible); + console.log('📄 [DIAGNOSTIC] Submit button visible:', submitVisible); + console.log('📄 [DIAGNOSTIC] Page contient "form":', hasForm); + + if (!report.formVisible) { + console.error('❌ [DIAGNOSTIC] Le formulaire de login n\'est pas visible'); + + // Logger le HTML pour debug + const bodyText = await page.locator('body').textContent(); + console.log('📄 [DIAGNOSTIC] Contenu de la page (premiers 500 chars):', bodyText?.substring(0, 500)); + + // Logger toutes les erreurs console capturĂ©es + if (consoleMessages.length > 0) { + console.log('\n🔮 [DIAGNOSTIC] Messages console capturĂ©s:'); + consoleMessages.forEach((msg) => { + console.log(` [${msg.type}] ${msg.text}`); + }); + } + + // VĂ©rifier s'il y a des scripts qui ont Ă©chouĂ© Ă  charger + const failedResources = await page.evaluate(() => { + const resources: Array<{ url: string; error: string }> = []; + const scripts = document.querySelectorAll('script[src]'); + scripts.forEach((script) => { + const src = script.getAttribute('src'); + if (src && !(script as any).loaded) { + resources.push({ url: src, error: 'Script not loaded' }); + } + }); + return resources; + }); + + if (failedResources.length > 0) { + console.log('🔮 [DIAGNOSTIC] Scripts non chargĂ©s:', failedResources); + } + + // Sauvegarder le rapport mĂȘme en cas d'Ă©chec + await page.evaluate((report) => { + (window as any).__diagnosticReport = report; + }, report); + + return; + } + + console.log('✅ [DIAGNOSTIC] Formulaire de login visible'); + + // Étape 2: Remplir le formulaire + console.log('🔍 [DIAGNOSTIC] Remplissage du formulaire...'); + await emailInput.fill(TEST_EMAIL); + await passwordInput.fill(TEST_PASSWORD); + + // VĂ©rifier si checkbox "remember me" existe + const rememberMeCheckbox = page.locator('input[type="checkbox"][id*="remember"]'); + if (await rememberMeCheckbox.count() > 0) { + await rememberMeCheckbox.check(); + } + + // Étape 3: Cliquer sur le bouton de connexion + console.log('🔍 [DIAGNOSTIC] Clic sur le bouton de connexion...'); + + // Attendre la navigation ou un message d'erreur + const navigationPromise = page.waitForURL( + (url) => url.pathname === '/dashboard' || url.pathname === '/', + { timeout: 10000 } + ).catch(() => null); + + const errorMessagePromise = page + .waitForSelector('.bg-red-100, [role="alert"], .text-red-700', { timeout: 5000 }) + .catch(() => null); + + await submitButton.click(); + + // Attendre soit la navigation, soit un message d'erreur + const navigationResult = await navigationPromise; + const errorElement = await errorMessagePromise; + + if (navigationResult) { + report.navigationSuccess = true; + report.finalUrl = page.url(); + console.log('✅ [DIAGNOSTIC] Navigation rĂ©ussie vers:', report.finalUrl); + } else if (errorElement) { + report.errorMessage = await errorElement.textContent() || 'Erreur inconnue'; + console.log('❌ [DIAGNOSTIC] Message d\'erreur dĂ©tectĂ©:', report.errorMessage); + } else { + // Attendre un peu plus pour voir si quelque chose se passe + await page.waitForTimeout(2000); + report.finalUrl = page.url(); + console.log('⚠ [DIAGNOSTIC] Pas de navigation ni d\'erreur visible. URL actuelle:', report.finalUrl); + } + + // Étape 4: VĂ©rifier le localStorage + console.log('🔍 [DIAGNOSTIC] VĂ©rification du localStorage...'); + const localStorageData = await context.storageState(); + const localStorageItems = await page.evaluate(() => { + const items: Record = {}; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key) { + items[key] = localStorage.getItem(key) || ''; + } + } + return items; + }); + + report.localStorage = localStorageItems; + + // VĂ©rifier spĂ©cifiquement les tokens + const hasAccessToken = 'access_token' in localStorageItems || + 'veza_access_token' in localStorageItems || + localStorageItems['access_token'] !== undefined || + localStorageItems['veza_access_token'] !== undefined; + + console.log('📩 [DIAGNOSTIC] LocalStorage:', Object.keys(localStorageItems)); + console.log(hasAccessToken ? '✅ [DIAGNOSTIC] Token d\'accĂšs prĂ©sent' : '❌ [DIAGNOSTIC] Token d\'accĂšs absent'); + + // GĂ©nĂ©rer le rapport + console.log('\n📊 [DIAGNOSTIC] === RAPPORT DE DIAGNOSTIC ==='); + console.log('Erreurs rĂ©seau:', report.networkErrors.length); + console.log('Erreurs console:', report.consoleErrors.length); + console.log('Erreurs CORS:', report.corsErrors.length); + console.log('Navigation rĂ©ussie:', report.navigationSuccess); + console.log('Token prĂ©sent:', hasAccessToken); + + // Afficher les dĂ©tails des erreurs + if (report.networkErrors.length > 0) { + console.log('\n🔮 Erreurs rĂ©seau:'); + report.networkErrors.forEach((err) => { + console.log(` - ${err.method} ${err.url}: ${err.error}`); + }); + } + + if (report.consoleErrors.length > 0) { + console.log('\n🔮 Erreurs console:'); + report.consoleErrors.forEach((err) => { + console.log(` - [${err.type}] ${err.message}`); + }); + } + + if (report.corsErrors.length > 0) { + console.log('\n🟠 Erreurs CORS:'); + report.corsErrors.forEach((err) => { + console.log(` - ${err.url}: ${err.reason}`); + }); + } + + // Sauvegarder le rapport pour l'analyse + await page.evaluate((report) => { + (window as any).__diagnosticReport = report; + }, report); + }); + + test.afterEach(async ({ page }) => { + // RĂ©cupĂ©rer le rapport depuis la page si disponible + const savedReport = await page.evaluate(() => { + return (window as any).__diagnosticReport; + }).catch(() => null); + + if (savedReport) { + report = savedReport; + } + }); +}); + diff --git a/apps/web/src/components/ErrorBoundary.tsx b/apps/web/src/components/ErrorBoundary.tsx index 044b7ad34..a99eda6d5 100644 --- a/apps/web/src/components/ErrorBoundary.tsx +++ b/apps/web/src/components/ErrorBoundary.tsx @@ -65,7 +65,7 @@ export class ErrorBoundary extends Component { - {process.env.NODE_ENV === 'development' && this.state.error && ( + {import.meta.env.DEV && this.state.error && (

Détails de l'erreur : diff --git a/apps/web/src/components/ui/LazyComponent.tsx b/apps/web/src/components/ui/LazyComponent.tsx index 58b5804bd..bafcd3716 100644 --- a/apps/web/src/components/ui/LazyComponent.tsx +++ b/apps/web/src/components/ui/LazyComponent.tsx @@ -26,8 +26,10 @@ export function createLazyComponent>( export const LazyDashboard = createLazyComponent(() => import('@/pages/DashboardPage').then((m) => ({ default: m.DashboardPage })), ); -export const LazyChat = createLazyComponent( - () => import('@/features/chat/pages/ChatPage'), +export const LazyChat = createLazyComponent(() => + import('@/features/chat/pages/ChatPage').then((m) => ({ + default: m.ChatPage, + })), ); export const LazyLibrary = createLazyComponent( () => import('@/features/library/pages/LibraryPage'), @@ -35,8 +37,10 @@ export const LazyLibrary = createLazyComponent( export const LazyProfile = createLazyComponent(() => import('@/pages/ProfilePage').then((m) => ({ default: m.ProfilePage })), ); -export const LazySettings = createLazyComponent( - () => import('@/features/settings/pages/SettingsPage'), +export const LazySettings = createLazyComponent(() => + import('@/features/settings/pages/SettingsPage').then((m) => ({ + default: m.SettingsPage, + })), ); export const LazyLogin = createLazyComponent(() => import('@/pages/LoginPage').then((m) => ({ default: m.LoginPage })), @@ -82,6 +86,8 @@ export const LazyPlaylistRoutes = createLazyComponent(() => default: m.PlaylistRoutes, })), ); -export const LazyMarketplace = createLazyComponent( - () => import('@/pages/marketplace/MarketplaceHome'), +export const LazyMarketplace = createLazyComponent(() => + import('@/pages/marketplace/MarketplaceHome').then((m) => ({ + default: m.MarketplaceHome, + })), ); diff --git a/apps/web/src/config/constants.ts b/apps/web/src/config/constants.ts index ee14f8941..95d0585da 100644 --- a/apps/web/src/config/constants.ts +++ b/apps/web/src/config/constants.ts @@ -4,10 +4,10 @@ // URLs de l'API export const API_URLS = { - BASE: process.env.VITE_API_URL || 'http://localhost:8080', - WS: process.env.VITE_WS_URL || 'ws://localhost:8081', - UPLOAD: process.env.VITE_UPLOAD_URL || 'http://localhost:8080/upload', - STREAM: process.env.VITE_STREAM_URL || 'http://localhost:8082', + BASE: import.meta.env.VITE_API_URL || 'http://127.0.0.1:8080', + WS: import.meta.env.VITE_WS_URL || 'ws://127.0.0.1:8081', + UPLOAD: import.meta.env.VITE_UPLOAD_URL || 'http://127.0.0.1:8080/upload', + STREAM: import.meta.env.VITE_STREAM_URL || 'ws://127.0.0.1:8082', } as const; // Endpoints de l'API diff --git a/apps/web/src/config/env.ts b/apps/web/src/config/env.ts index 9486b5836..8bc7e196c 100644 --- a/apps/web/src/config/env.ts +++ b/apps/web/src/config/env.ts @@ -3,10 +3,10 @@ import { z } from 'zod'; // Schéma de validation pour les variables d'environnement // Aligné avec FRONTEND_INTEGRATION.md const envSchema = z.object({ - VITE_API_URL: z.string().url().default('http://localhost:8080/api/v1'), - VITE_WS_URL: z.string().url().default('ws://localhost:8081/ws'), - VITE_STREAM_URL: z.string().url().default('ws://localhost:8082/stream'), - VITE_UPLOAD_URL: z.string().url().default('http://localhost:8080/upload'), + VITE_API_URL: z.string().url().default('http://127.0.0.1:8080/api/v1'), + VITE_WS_URL: z.string().url().default('ws://127.0.0.1:8081/ws'), + VITE_STREAM_URL: z.string().url().default('ws://127.0.0.1:8082/stream'), + VITE_UPLOAD_URL: z.string().url().default('http://127.0.0.1:8080/upload'), VITE_APP_NAME: z.string().default('Veza'), VITE_DEBUG: z .string() diff --git a/apps/web/src/features/chat/hooks/useChat.ts b/apps/web/src/features/chat/hooks/useChat.ts index 406ce6f25..347091d99 100644 --- a/apps/web/src/features/chat/hooks/useChat.ts +++ b/apps/web/src/features/chat/hooks/useChat.ts @@ -31,7 +31,7 @@ export const useChat = () => { ws.current.onopen = () => { setWsStatus('connected'); - console.log('WebSocket connected'); + // WebSocket connection successful - no logging needed in production // Send any queued messages setMessagesToSend((prev) => { prev.forEach((msg) => ws.current?.send(JSON.stringify(msg))); @@ -59,7 +59,7 @@ export const useChat = () => { ws.current.onclose = () => { setWsStatus('disconnected'); - console.log('WebSocket disconnected'); + // WebSocket disconnected - no logging needed in production // Optional: Reconnect logic }; @@ -96,7 +96,7 @@ export const useChat = () => { !currentConversationId || !userId ) { - console.warn('WebSocket not open, cannot send message'); + // WebSocket not ready - message will be queued // Queue message to send later setMessagesToSend((prev) => [ ...prev, diff --git a/apps/web/src/features/library/components/LibraryManager.tsx b/apps/web/src/features/library/components/LibraryManager.tsx index 6d639ae29..c9ff08f3d 100644 --- a/apps/web/src/features/library/components/LibraryManager.tsx +++ b/apps/web/src/features/library/components/LibraryManager.tsx @@ -109,7 +109,8 @@ export function LibraryManager({ onTrackSelect }: LibraryManagerProps) { if (originalTrack) { // setSelectedTrack(originalTrack); // setIsEditDialogOpen(true); - console.log('Edit track', originalTrack); // Temporary + // TODO: Implement edit track functionality + // Removed temporary console.log for production } }; diff --git a/apps/web/src/features/player/components/PlayerError.tsx b/apps/web/src/features/player/components/PlayerError.tsx index 06877176c..66fd9dcdc 100644 --- a/apps/web/src/features/player/components/PlayerError.tsx +++ b/apps/web/src/features/player/components/PlayerError.tsx @@ -123,7 +123,7 @@ export function PlayerError({ )} - {process.env.NODE_ENV === 'development' && ( + {import.meta.env.DEV && (
Détails techniques diff --git a/apps/web/src/features/player/hooks/useStreamSync.ts b/apps/web/src/features/player/hooks/useStreamSync.ts index 5a2a8c8ef..1d9442946 100644 --- a/apps/web/src/features/player/hooks/useStreamSync.ts +++ b/apps/web/src/features/player/hooks/useStreamSync.ts @@ -62,11 +62,8 @@ export function useStreamSync(params: { const targetTime = currentSec - correctionSec; - if (import.meta.env.DEV) { - console.log( - `[Sync] Correcting drift: ${driftMs}ms. Seek ${currentSec} -> ${targetTime}`, - ); - } + // Stream sync correction - logging removed for production + // Debug info available via onDebug callback if needed audioPlayerService.seek(targetTime); } }, diff --git a/apps/web/src/features/playlists/hooks/usePlaylistPermissions.ts b/apps/web/src/features/playlists/hooks/usePlaylistPermissions.ts index 59e10e08f..6cffe142a 100644 --- a/apps/web/src/features/playlists/hooks/usePlaylistPermissions.ts +++ b/apps/web/src/features/playlists/hooks/usePlaylistPermissions.ts @@ -7,7 +7,15 @@ export function usePlaylistPermissions(playlist?: Playlist) { return useMemo(() => { if (!playlist || !user) { - return { canEdit: false, canDelete: false }; + return { + canEdit: false, + canDelete: false, + canAddTracks: false, + canRemoveTracks: false, + canManageCollaborators: false, + canRead: false, + isOwner: false, + }; } const isOwner = String(playlist.user_id) === String(user.id); @@ -16,6 +24,11 @@ export function usePlaylistPermissions(playlist?: Playlist) { return { canEdit: isOwner, canDelete: isOwner, + canAddTracks: isOwner, + canRemoveTracks: isOwner, + canManageCollaborators: isOwner, + canRead: true, // Anyone can read public playlists, owner can read private ones + isOwner, }; }, [playlist, user]); } diff --git a/apps/web/src/features/playlists/pages/PlaylistDetailPage.tsx b/apps/web/src/features/playlists/pages/PlaylistDetailPage.tsx new file mode 100644 index 000000000..1f22cb469 --- /dev/null +++ b/apps/web/src/features/playlists/pages/PlaylistDetailPage.tsx @@ -0,0 +1,127 @@ +import { useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { usePlaylist, useCollaborators } from '../hooks/usePlaylist'; +import { usePlaylistPermissions } from '../hooks/usePlaylistPermissions'; +import { PlaylistHeader } from '../components/PlaylistHeader'; +import { PlaylistActions } from '../components/PlaylistActions'; +import { PlaylistTrackList } from '../components/PlaylistTrackList'; +import { AddTrackToPlaylistModal } from '../components/AddTrackToPlaylistModal'; +import { Button } from '@/components/ui/button'; +import { LoadingSpinner } from '@/components/ui/loading-spinner'; +import { Card, CardContent } from '@/components/ui/card'; +import { Plus } from 'lucide-react'; +import { toast } from 'react-hot-toast'; + +export function PlaylistDetailPage() { + const { id } = useParams<{ id: string }>(); + const [isAddTrackModalOpen, setIsAddTrackModalOpen] = useState(false); + const [isShareModalOpen, setIsShareModalOpen] = useState(false); + + const { + data: playlist, + isLoading, + error, + refetch, + } = usePlaylist(id || ''); + + const { data: collaborators } = useCollaborators(id || ''); + + const permissions = usePlaylistPermissions(playlist); + + if (isLoading) { + return ( +
+
+ +
+
+ ); + } + + if (error || !playlist) { + return ( +
+ + +
+

Error

+

+ {error instanceof Error + ? error.message + : 'Failed to load playlist'} +

+
+
+
+
+ ); + } + + const tracks = playlist.tracks?.map((pt) => pt.track).filter(Boolean) || []; + const playlistTracks = playlist.tracks || []; + + const handleTrackAdded = () => { + setIsAddTrackModalOpen(false); + refetch(); + toast.success('Track added to playlist'); + }; + + const handleTrackRemoved = () => { + refetch(); + toast.success('Track removed from playlist'); + }; + + const handleTracksReordered = () => { + refetch(); + toast.success('Playlist tracks reordered'); + }; + + const handleShareClick = () => { + setIsShareModalOpen(true); + }; + + return ( +
+ + +
+ +
+ +
+

Tracks

+ {permissions.canAddTracks && ( + + )} +
+ + + + setIsAddTrackModalOpen(false)} + playlistId={playlist.id} + onTracksAdded={handleTrackAdded} + /> +
+ ); +} diff --git a/apps/web/src/features/playlists/routes.tsx b/apps/web/src/features/playlists/routes.tsx new file mode 100644 index 000000000..78a0187cf --- /dev/null +++ b/apps/web/src/features/playlists/routes.tsx @@ -0,0 +1,15 @@ +import { Routes, Route, Navigate } from 'react-router-dom'; +import { PlaylistListPage } from './pages/PlaylistListPage'; +import { PlaylistDetailPage } from './pages/PlaylistDetailPage'; + +export function PlaylistRoutes() { + return ( + + } /> + } /> + } /> + } /> + } /> + + ); +} diff --git a/apps/web/src/features/profile/pages/UserProfilePage.tsx b/apps/web/src/features/profile/pages/UserProfilePage.tsx new file mode 100644 index 000000000..8ceacd7d5 --- /dev/null +++ b/apps/web/src/features/profile/pages/UserProfilePage.tsx @@ -0,0 +1,154 @@ +import { useParams } from 'react-router-dom'; +import { useQuery } from '@tanstack/react-query'; +import { getProfileByUsername, type UserProfile } from '../services/profileService'; +import { LoadingSpinner } from '@/components/ui/loading-spinner'; +import { FollowButton } from '../components/FollowButton'; +import { format } from 'date-fns'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; + +export function UserProfilePage() { + const { username } = useParams<{ username: string }>(); + + const { + data: profile, + isLoading, + error, + } = useQuery({ + queryKey: ['userProfile', username], + queryFn: async () => { + if (!username) { + throw new Error('Username is required'); + } + return getProfileByUsername(username); + }, + enabled: !!username, + retry: false, + }); + + if (isLoading) { + return ( +
+
+
+ +

Loading profile...

+
+
+
+ ); + } + + if (error || !username) { + return ( +
+ + +
+

Error

+

+ {error instanceof Error + ? error.message + : !username + ? 'Username is required' + : 'Failed to load profile'} +

+
+
+
+
+ ); + } + + if (!profile) { + return ( +
+ + +
+

User Not Found

+

+ The user profile you're looking for doesn't exist. +

+
+
+
+
+ ); + } + + const displayName = + profile.first_name || profile.last_name + ? `${profile.first_name || ''} ${profile.last_name || ''}`.trim() + : profile.username; + + const initials = displayName + .split(' ') + .map((n) => n[0]) + .join('') + .toUpperCase() + .slice(0, 2); + + const memberSince = profile.created_at + ? format(new Date(profile.created_at), 'M/d/yyyy') + : null; + + return ( +
+
+ + +
+
+ + + {initials} + +
+ {profile.username} + {displayName !== profile.username && ( +

{displayName}

+ )} + {memberSince && ( +

+ Member since {memberSince} +

+ )} +
+
+ +
+
+ +
+ {profile.bio && ( +
+

Bio

+

+ {profile.bio} +

+
+ )} + + {profile.location && ( +
+

Location

+

{profile.location}

+
+ )} + + {!profile.bio && !profile.location && ( +

+ No additional information available. +

+ )} +
+
+
+
+
+ ); +} diff --git a/apps/web/src/features/roles/pages/RolesPage.tsx b/apps/web/src/features/roles/pages/RolesPage.tsx new file mode 100644 index 000000000..2625fb6ab --- /dev/null +++ b/apps/web/src/features/roles/pages/RolesPage.tsx @@ -0,0 +1,208 @@ +import { useState, useEffect } from 'react'; +import { getRoles, deleteRole, updateRole } from '../services/roleService'; +import type { Role } from '../types/role'; +import { Button } from '@/components/ui/button'; +import { LoadingSpinner } from '@/components/ui/loading-spinner'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { Badge } from '@/components/ui/badge'; +import { toast } from 'react-hot-toast'; +import { Shield, Edit, Trash2, Plus } from 'lucide-react'; + +export function RolesPage() { + const [roles, setRoles] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const loadRoles = async () => { + try { + setIsLoading(true); + setError(null); + const loadedRoles = await getRoles(); + setRoles(loadedRoles); + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : 'Failed to load roles'; + setError(errorMessage); + toast.error(errorMessage); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + loadRoles(); + }, []); + + const handleToggleActive = async (role: Role) => { + try { + await updateRole(role.id, { is_active: !role.is_active }); + toast.success( + `Role ${!role.is_active ? 'activated' : 'deactivated'} successfully`, + ); + loadRoles(); + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : 'Failed to update role'; + toast.error(errorMessage); + } + }; + + const handleDelete = async (role: Role) => { + if (role.is_system) { + toast.error('Cannot delete system roles'); + return; + } + + if (!confirm(`Are you sure you want to delete role "${role.display_name}"?`)) { + return; + } + + try { + await deleteRole(role.id); + toast.success('Role deleted successfully'); + loadRoles(); + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : 'Failed to delete role'; + toast.error(errorMessage); + } + }; + + if (isLoading) { + return ( +
+
+ +
+
+ ); + } + + if (error) { + return ( +
+ + +
+

Error

+

{error}

+ +
+
+
+
+ ); + } + + return ( +
+
+
+

Roles Management

+

+ Manage user roles and permissions +

+
+ +
+ + + + + + Roles + + + + {roles.length === 0 ? ( +
+

No roles found

+
+ ) : ( + + + + Name + Display Name + Description + Status + Type + Permissions + Actions + + + + {roles.map((role) => ( + + {role.name} + {role.display_name} + + {role.description} + + + + {role.is_active ? 'Active' : 'Inactive'} + + + + {role.is_system ? ( + System + ) : ( + Custom + )} + + + {role.permissions?.length || 0} permissions + + +
+ + + +
+
+
+ ))} +
+
+ )} +
+
+
+ ); +} diff --git a/apps/web/src/features/settings/components/PreferenceSettings.tsx b/apps/web/src/features/settings/components/PreferenceSettings.tsx new file mode 100644 index 000000000..3c2f062c5 --- /dev/null +++ b/apps/web/src/features/settings/components/PreferenceSettings.tsx @@ -0,0 +1,137 @@ +import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { + RadioGroup, + RadioGroupItem, +} from '@/components/ui/radio-group'; +import { PreferenceSettings as PreferenceSettingsType } from '../types/settings'; + +interface PreferenceSettingsProps { + preferences: PreferenceSettingsType; + onChange: (preferences: PreferenceSettingsType) => void; +} + +const supportedLanguages = [ + { value: 'en', label: 'English' }, + { value: 'fr', label: 'Français' }, + { value: 'es', label: 'Español' }, + { value: 'de', label: 'Deutsch' }, + { value: 'it', label: 'Italiano' }, + { value: 'pt', label: 'PortuguĂȘs' }, + { value: 'ru', label: 'РуссĐșĐžĐč' }, + { value: 'ja', label: 'æ—„æœŹèȘž' }, + { value: 'zh', label: 'äž­æ–‡' }, + { value: 'ko', label: '한ꔭ얎' }, +]; + +const commonTimezones = [ + { value: 'UTC', label: 'UTC' }, + { value: 'Europe/Paris', label: 'Europe/Paris' }, + { value: 'America/New_York', label: 'America/New_York' }, + { value: 'America/Los_Angeles', label: 'America/Los_Angeles' }, + { value: 'Asia/Tokyo', label: 'Asia/Tokyo' }, + { value: 'Asia/Shanghai', label: 'Asia/Shanghai' }, +]; + +export function PreferenceSettings({ + preferences, + onChange, +}: PreferenceSettingsProps) { + const handleLanguageChange = (value: string) => { + onChange({ + ...preferences, + language: value, + }); + }; + + const handleTimezoneChange = (value: string) => { + onChange({ + ...preferences, + timezone: value, + }); + }; + + const handleThemeChange = (value: string) => { + onChange({ + ...preferences, + theme: value as 'light' | 'dark' | 'auto', + }); + }; + + return ( +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ ); +} diff --git a/apps/web/src/features/settings/components/SettingsTabs.tsx b/apps/web/src/features/settings/components/SettingsTabs.tsx new file mode 100644 index 000000000..b4669d337 --- /dev/null +++ b/apps/web/src/features/settings/components/SettingsTabs.tsx @@ -0,0 +1,82 @@ +import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; +import { UserSettings } from '../types/settings'; +import { PreferenceSettings } from './PreferenceSettings'; +import { NotificationSettings } from './NotificationSettings'; +import { PrivacySettings } from './PrivacySettings'; +import { ContentSettings } from './ContentSettings'; + +interface SettingsTabsProps { + settings: UserSettings; + onChange: (settings: UserSettings) => void; +} + +export function SettingsTabs({ settings, onChange }: SettingsTabsProps) { + const handlePreferencesChange = (preferences: typeof settings.preferences) => { + onChange({ + ...settings, + preferences, + }); + }; + + const handleNotificationsChange = ( + notifications: typeof settings.notifications, + ) => { + onChange({ + ...settings, + notifications, + }); + }; + + const handlePrivacyChange = (privacy: typeof settings.privacy) => { + onChange({ + ...settings, + privacy, + }); + }; + + const handleContentChange = (content: typeof settings.content) => { + onChange({ + ...settings, + content, + }); + }; + + return ( + + + PrĂ©fĂ©rences + Notifications + ConfidentialitĂ© + Contenu + + + + + + + + + + + + + + + + + + + ); +} diff --git a/apps/web/src/features/settings/pages/SettingsPage.tsx b/apps/web/src/features/settings/pages/SettingsPage.tsx new file mode 100644 index 000000000..53bada08c --- /dev/null +++ b/apps/web/src/features/settings/pages/SettingsPage.tsx @@ -0,0 +1,130 @@ +import { useState, useEffect } from 'react'; +import { useAuthStore } from '@/stores/auth'; +import { getSettings, updateSettings } from '../services/settingsService'; +import { UserSettings } from '../types/settings'; +import { SettingsTabs } from '../components/SettingsTabs'; +import { Button } from '@/components/ui/button'; +import { LoadingSpinner } from '@/components/ui/loading-spinner'; +import { toast } from 'react-hot-toast'; +import { settingsSchema } from '../schemas/settingsSchema'; + +export function SettingsPage() { + const { user } = useAuthStore(); + const [settings, setSettings] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (!user?.id) { + setError('Vous devez ĂȘtre connectĂ© pour accĂ©der aux paramĂštres'); + setIsLoading(false); + return; + } + + const loadSettings = async () => { + try { + setIsLoading(true); + setError(null); + const userSettings = await getSettings(user.id); + setSettings(userSettings); + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : 'Erreur de chargement des paramĂštres'; + setError(errorMessage); + toast.error(errorMessage); + } finally { + setIsLoading(false); + } + }; + + loadSettings(); + }, [user?.id]); + + const handleSave = async () => { + if (!user?.id || !settings) { + return; + } + + // Valider les settings avec le schĂ©ma Zod + const validationResult = settingsSchema.safeParse(settings); + if (!validationResult.success) { + const errors = validationResult.error.errors + .map((e) => `${e.path.join('.')}: ${e.message}`) + .join(', '); + toast.error(`Erreur de validation: ${errors}`); + return; + } + + try { + setIsSaving(true); + await updateSettings(user.id, settings); + toast.success('ParamĂštres sauvegardĂ©s avec succĂšs'); + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : 'Erreur lors de la sauvegarde'; + toast.error(errorMessage); + } finally { + setIsSaving(false); + } + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error && !settings) { + return ( +
+
+

ParamĂštres

+
+ {error} +
+
+
+ ); + } + + if (!settings) { + return null; + } + + return ( +
+
+
+

ParamĂštres

+

+ Gérez vos paramÚtres de compte et préférences +

+
+ +
+ + +
+ +
+
+
+
+ ); +} diff --git a/apps/web/src/features/streaming/hooks/usePlaybackRealtime.ts b/apps/web/src/features/streaming/hooks/usePlaybackRealtime.ts index b7f6ee89d..a6d80803b 100644 --- a/apps/web/src/features/streaming/hooks/usePlaybackRealtime.ts +++ b/apps/web/src/features/streaming/hooks/usePlaybackRealtime.ts @@ -144,7 +144,17 @@ export function usePlaybackRealtime( * Construit l'URL WebSocket pour le track */ const getWebSocketUrl = useCallback((trackId: number): string => { - const apiBaseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080'; + const apiBaseUrl = (() => { + const url = import.meta.env.VITE_API_URL; + if (!url) { + if (import.meta.env.PROD) { + throw new Error('VITE_API_URL must be defined in production'); + } + // Fallback uniquement en développement + return 'http://127.0.0.1:8080'; + } + return url; + })(); // Convertir http:// en ws:// ou https:// en wss:// const wsBaseUrl = apiBaseUrl.replace(/^http/, 'ws'); // Récupérer le token d'authentification depuis le client API diff --git a/apps/web/src/features/tracks/pages/TrackDetailPage.tsx b/apps/web/src/features/tracks/pages/TrackDetailPage.tsx new file mode 100644 index 000000000..60284191f --- /dev/null +++ b/apps/web/src/features/tracks/pages/TrackDetailPage.tsx @@ -0,0 +1,262 @@ +import { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { getTrack, TrackUploadError } from '../services/trackService'; +import { usePlayerStore } from '@/features/player/store/playerStore'; +import type { Track as PlayerTrack } from '@/features/player/types'; +import { toast } from 'react-hot-toast'; +import { Button } from '@/components/ui/button'; +import { LoadingSpinner } from '@/components/ui/loading-spinner'; +import { Card, CardContent } from '@/components/ui/card'; +import { Play, Pause, ArrowLeft, Share2, Plus } from 'lucide-react'; +import { Music } from 'lucide-react'; +import type { Track } from '../types/track'; + +function formatDuration(seconds: number): string { + const minutes = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${minutes}:${secs.toString().padStart(2, '0')}`; +} + +export function TrackDetailPage() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const [track, setTrack] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const { play, pause, currentTrack, isPlaying, addToQueue } = usePlayerStore(); + + useEffect(() => { + if (!id) { + setError('Track ID is required'); + setIsLoading(false); + return; + } + + const loadTrack = async () => { + try { + setIsLoading(true); + setError(null); + const trackId = Number(id); + if (isNaN(trackId)) { + throw new Error('Invalid track ID'); + } + const loadedTrack = await getTrack(trackId); + setTrack(loadedTrack); + } catch (err) { + const errorMessage = + err instanceof TrackUploadError + ? err.message + : err instanceof Error + ? err.message + : 'Failed to load track'; + setError(errorMessage); + if (err instanceof TrackUploadError) { + toast.error(errorMessage); + } + } finally { + setIsLoading(false); + } + }; + + loadTrack(); + }, [id]); + + const mapToPlayerTrack = (t: Track): PlayerTrack => ({ + id: t.id, + title: t.title, + artist: t.artist, + album: t.album, + duration: t.duration, + url: t.stream_manifest_url || t.file_path, + cover: t.cover_art_path, + genre: t.genre, + }); + + const handlePlay = () => { + if (!track) return; + const playerTrack = mapToPlayerTrack(track); + play(playerTrack); + }; + + const handlePause = () => { + pause(); + }; + + const handleAddToQueue = () => { + if (!track) return; + const playerTrack = mapToPlayerTrack(track); + addToQueue([playerTrack]); + toast.success('Track ajouté à la file d\'attente'); + }; + + const handleShare = async () => { + if (!track) return; + const shareUrl = `${window.location.origin}/tracks/${track.id}`; + try { + await navigator.clipboard.writeText(shareUrl); + toast.success('Lien copié dans le presse-papiers'); + } catch (err) { + toast.error('Impossible de copier le lien'); + } + }; + + const isCurrentTrack = currentTrack?.id === track?.id; + const isCurrentlyPlaying = isCurrentTrack && isPlaying; + + if (isLoading) { + return ( +
+
+ + Chargement du track... +
+
+ ); + } + + if (error || !track) { + return ( +
+ + +
+

Error

+

+ {error || 'Track introuvable'} +

+ +
+
+
+
+ ); + } + + return ( +
+ + +
+ {/* Cover Art */} +
+ + + {track.cover_art_path ? ( + {track.title} + ) : ( +
+ +
+ )} +
+
+
+ + {/* Track Details */} +
+ + +

{track.title}

+

{track.artist}

+ {track.album && ( +

Album: {track.album}

+ )} + +
+ {isCurrentlyPlaying ? ( + + ) : ( + + )} + + +
+ +
+
+

Durée

+

{formatDuration(track.duration)}

+
+ {track.genre && ( +
+

Genre

+

{track.genre}

+
+ )} + {track.year && ( +
+

Année

+

{track.year}

+
+ )} +
+

Lectures

+

{track.play_count}

+
+
+

Likes

+

{track.like_count}

+
+
+
+
+ + {/* Waveform */} + {track.waveform_path && ( + + +

Waveform

+ Waveform +
+
+ )} +
+
+
+ ); +} diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index d451bc4d8..92ca20ae0 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -5,6 +5,8 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Toaster } from 'react-hot-toast'; import { App } from './app/App'; import './index.css'; +// Initialize i18next before React renders +import './lib/i18n'; // HMR Force Update: 1765126900 @@ -20,7 +22,12 @@ const queryClient = new QueryClient({ ReactDOM.createRoot(document.getElementById('root')!).render( - + diff --git a/apps/web/src/pages/DashboardPage.tsx b/apps/web/src/pages/DashboardPage.tsx index c7ec55b11..230950ccc 100644 --- a/apps/web/src/pages/DashboardPage.tsx +++ b/apps/web/src/pages/DashboardPage.tsx @@ -21,6 +21,9 @@ export function DashboardPage() { fetchItems({ limit: 5 }); }, [fetchItems]); + // Sécuriser items pour s'assurer que c'est toujours un tableau + const safeItems = Array.isArray(items) ? items : []; + const stats = [ { title: 'Pistes écoutées', @@ -152,7 +155,7 @@ export function DashboardPage() {

) : (
- {items.slice(0, 3).map((item) => ( + {safeItems.slice(0, 3).map((item) => (
@@ -167,7 +170,7 @@ export function DashboardPage() {
))} - {items.length === 0 && ( + {safeItems.length === 0 && (

Aucune piste dans votre bibliothĂšque

diff --git a/apps/web/src/pages/marketplace/MarketplaceHome.tsx b/apps/web/src/pages/marketplace/MarketplaceHome.tsx new file mode 100644 index 000000000..095e8a9a7 --- /dev/null +++ b/apps/web/src/pages/marketplace/MarketplaceHome.tsx @@ -0,0 +1,92 @@ +import { useState, useEffect } from 'react'; +import { marketplaceService } from '@/services/marketplaceService'; +import { ProductCard } from '@/features/marketplace/components/ProductCard'; +import { Product } from '@/types/marketplace'; +import { LoadingSpinner } from '@/components/ui/loading-spinner'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { toast } from 'react-hot-toast'; + +export function MarketplaceHome() { + const [products, setProducts] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [purchasingProductId, setPurchasingProductId] = useState(null); + + useEffect(() => { + const loadProducts = async () => { + try { + setIsLoading(true); + const fetchedProducts = await marketplaceService.fetchProducts(); + setProducts(fetchedProducts); + } catch (error) { + const errorMessage = + error instanceof Error + ? error.message + : 'Failed to load marketplace products'; + toast.error(errorMessage); + } finally { + setIsLoading(false); + } + }; + + loadProducts(); + }, []); + + const handlePurchase = async (product: Product) => { + try { + setPurchasingProductId(product.id); + await marketplaceService.purchaseProduct(product.id); + toast.success(`Successfully purchased ${product.title}`); + // Optionally refresh products or update UI + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Failed to purchase product'; + toast.error(errorMessage); + } finally { + setPurchasingProductId(null); + } + }; + + if (isLoading) { + return ( +
+
+ +
+
+ ); + } + + return ( +
+
+

Marketplace

+

+ Discover and purchase music products, samples, and licenses +

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

+ No products available at the moment. +

+
+
+
+ ) : ( +
+ {products.map((product) => ( + + ))} +
+ )} +
+ ); +} diff --git a/apps/web/src/router/index.tsx b/apps/web/src/router/index.tsx index 24ac164bd..906dabab8 100644 --- a/apps/web/src/router/index.tsx +++ b/apps/web/src/router/index.tsx @@ -4,6 +4,7 @@ import { useAuth } from '@/features/auth/hooks/useAuth'; import { ProtectedRoute } from '@/components/auth/ProtectedRoute'; import { DashboardLayout } from '@/components/layout/DashboardLayout'; import { LoadingSpinner } from '@/components/ui/loading-spinner'; +import { ErrorBoundary } from '@/components/ErrorBoundary'; import { LazyLogin, LazyRegister, @@ -126,7 +127,9 @@ export const AppRouter = () => ( element={ - + + + } @@ -136,7 +139,9 @@ export const AppRouter = () => ( element={ - + + + } @@ -146,7 +151,9 @@ export const AppRouter = () => ( element={ - + + + } diff --git a/apps/web/src/services/api.ts b/apps/web/src/services/api.ts index 74e9bb96f..6d5d60688 100644 --- a/apps/web/src/services/api.ts +++ b/apps/web/src/services/api.ts @@ -15,9 +15,30 @@ import type { export type { Track }; // Configuration de base -const API_BASE_URL = - import.meta.env.VITE_API_URL || 'http://localhost:8080/api/v1'; -const WS_BASE_URL = import.meta.env.VITE_WS_URL || 'ws://localhost:8081/ws'; +// En production, les variables d'environnement doivent ĂȘtre dĂ©finies +const API_BASE_URL = (() => { + const url = import.meta.env.VITE_API_URL; + if (!url) { + if (import.meta.env.PROD) { + throw new Error('VITE_API_URL must be defined in production'); + } + // Fallback uniquement en dĂ©veloppement + return 'http://127.0.0.1:8080/api/v1'; + } + return url; +})(); + +const WS_BASE_URL = (() => { + const url = import.meta.env.VITE_WS_URL; + if (!url) { + if (import.meta.env.PROD) { + throw new Error('VITE_WS_URL must be defined in production'); + } + // Fallback uniquement en dĂ©veloppement + return 'ws://127.0.0.1:8081/ws'; + } + return url; +})(); // SchĂ©mas de validation Zod const UserSchema = z.object({ @@ -270,12 +291,14 @@ export class ApiService { } // MĂ©thodes pour la bibliothĂšque + // Note: Le backend n'a pas d'endpoint /library, on utilise /tracks Ă  la place async getLibraryItems(params?: { page?: number; limit?: number; type?: string; }): Promise> { - const response = await this.client.get('/library', { params }); + // Utiliser /tracks au lieu de /library qui n'existe pas + const response = await this.client.get('/tracks', { params }); return response.data; } diff --git a/apps/web/src/services/secure-auth.ts b/apps/web/src/services/secure-auth.ts index a8ad89c4e..e39e730c8 100644 --- a/apps/web/src/services/secure-auth.ts +++ b/apps/web/src/services/secure-auth.ts @@ -49,8 +49,17 @@ const AuthTokensSchema = z.object({ }); // Configuration -const API_BASE_URL = - import.meta.env.VITE_API_URL || 'http://localhost:8080/api/v1'; +const API_BASE_URL = (() => { + const url = import.meta.env.VITE_API_URL; + if (!url) { + if (import.meta.env.PROD) { + throw new Error('VITE_API_URL must be defined in production'); + } + // Fallback uniquement en dĂ©veloppement + return 'http://127.0.0.1:8080/api/v1'; + } + return url; +})(); export class SecureAuthService { private static instance: SecureAuthService; diff --git a/apps/web/src/services/tokenRefresh.ts b/apps/web/src/services/tokenRefresh.ts index 7b6577380..8a316a209 100644 --- a/apps/web/src/services/tokenRefresh.ts +++ b/apps/web/src/services/tokenRefresh.ts @@ -7,8 +7,20 @@ let refreshClient: AxiosInstance | null = null; function getRefreshClient(): AxiosInstance { if (!refreshClient) { + const baseURL = (() => { + const url = import.meta.env.VITE_API_URL; + if (!url) { + if (import.meta.env.PROD) { + throw new Error('VITE_API_URL must be defined in production'); + } + // Fallback uniquement en dĂ©veloppement + return 'http://127.0.0.1:8080/api/v1'; + } + return url; + })(); + refreshClient = axios.create({ - baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8080/api/v1', + baseURL, timeout: 10000, headers: { 'Content-Type': 'application/json', diff --git a/apps/web/src/services/websocket.ts b/apps/web/src/services/websocket.ts index b853fe428..f65cfa0af 100644 --- a/apps/web/src/services/websocket.ts +++ b/apps/web/src/services/websocket.ts @@ -44,7 +44,17 @@ class WebSocketServiceImpl implements WebSocketService { } // CORRECTION: Le serveur Rust expose le WebSocket sur /ws - const wsUrl = import.meta.env.VITE_WS_URL || 'ws://localhost:8081/ws'; + const wsUrl = (() => { + const url = import.meta.env.VITE_WS_URL; + if (!url) { + if (import.meta.env.PROD) { + throw new Error('VITE_WS_URL must be defined in production'); + } + // Fallback uniquement en dĂ©veloppement + return 'ws://127.0.0.1:8081/ws'; + } + return url; + })(); return new Promise((resolve, reject) => { try { diff --git a/apps/web/src/stores/library.ts b/apps/web/src/stores/library.ts index c6afc9c4f..d678aa8bd 100644 --- a/apps/web/src/stores/library.ts +++ b/apps/web/src/stores/library.ts @@ -73,20 +73,29 @@ export const useLibraryStore = create( type, }); + // SĂ©curiser response.data pour s'assurer que c'est toujours un tableau + const itemsArray = Array.isArray(response.data) + ? response.data + : Array.isArray(response) + ? response + : []; + set({ - items: response.data, + items: itemsArray, pagination: { - page: response.page, - limit: response.limit, - total: response.total, - hasNext: response.has_next, - hasPrev: response.has_prev, + page: response.page || 1, + limit: response.limit || limit, + total: response.total || 0, + hasNext: response.has_next || false, + hasPrev: response.has_prev || false, }, isLoading: false, error: null, }); } catch (error: any) { + // En cas d'erreur, s'assurer que items reste un tableau vide set({ + items: [], error: error as ApiError, isLoading: false, }); @@ -103,13 +112,22 @@ export const useLibraryStore = create( type: 'favorites', }); + // SĂ©curiser response.data pour s'assurer que c'est toujours un tableau + const favoritesArray = Array.isArray(response.data) + ? response.data + : Array.isArray(response) + ? response + : []; + set({ - favorites: response.data, + favorites: favoritesArray, isLoading: false, error: null, }); } catch (error: any) { + // En cas d'erreur, s'assurer que favorites reste un tableau vide set({ + favorites: [], error: error as ApiError, isLoading: false, }); diff --git a/apps/web/src/utils/csp.ts b/apps/web/src/utils/csp.ts index c92d147fa..2ac8550bf 100644 --- a/apps/web/src/utils/csp.ts +++ b/apps/web/src/utils/csp.ts @@ -159,7 +159,7 @@ export function createCSPMiddleware() { setCSPNonce(nonce); const cspHeader = - process.env.NODE_ENV === 'production' + import.meta.env.MODE === 'production' ? buildCSPHeader(nonce) : buildCSPHeaderDev(); diff --git a/apps/web/src/utils/logger.ts b/apps/web/src/utils/logger.ts new file mode 100644 index 000000000..c1cfaab5b --- /dev/null +++ b/apps/web/src/utils/logger.ts @@ -0,0 +1,50 @@ +/** + * Logger conditionnel pour la production + * Remplace console.log/info/warn/error par des fonctions conditionnelles + * En production, seuls les erreurs critiques sont loggĂ©es + */ + +type LogLevel = 'debug' | 'info' | 'warn' | 'error'; + +interface Logger { + debug: (...args: unknown[]) => void; + info: (...args: unknown[]) => void; + warn: (...args: unknown[]) => void; + error: (...args: unknown[]) => void; +} + +const isDev = import.meta.env.DEV; +const isProd = import.meta.env.PROD; + +/** + * Logger conditionnel qui filtre les logs selon l'environnement + */ +export const logger: Logger = { + debug: (...args: unknown[]) => { + if (isDev) { + console.debug('[DEBUG]', ...args); + } + }, + info: (...args: unknown[]) => { + if (isDev) { + console.info('[INFO]', ...args); + } + }, + warn: (...args: unknown[]) => { + // Warnings toujours loggĂ©s, mais avec prĂ©fixe en prod + if (isProd) { + console.warn('[WARN]', ...args); + } else { + console.warn('[WARN]', ...args); + } + }, + error: (...args: unknown[]) => { + // Erreurs toujours loggĂ©es en production pour le debugging + console.error('[ERROR]', ...args); + }, +}; + +/** + * Export par dĂ©faut pour faciliter l'import + */ +export default logger; diff --git a/apps/web/src/utils/sanitize.ts b/apps/web/src/utils/sanitize.ts index 77e531f7c..10e15ef0e 100644 --- a/apps/web/src/utils/sanitize.ts +++ b/apps/web/src/utils/sanitize.ts @@ -2,6 +2,7 @@ * XSS Protection utilities * Sanitise et valide le contenu utilisateur pour prĂ©venir les attaques XSS */ +import DOMPurify from 'dompurify'; // Types pour la configuration de sanitisation export interface SanitizeOptions { @@ -268,7 +269,25 @@ function escapeHTML(content: string): string { /** * Sanitise spĂ©cifiquement les messages de chat */ +/** + * Sanitise les messages de chat avec DOMPurify pour une protection XSS robuste + * Utilise DOMPurify en prioritĂ©, avec fallback sur sanitizeHTML si DOMPurify n'est pas disponible + */ export function sanitizeChatMessage(message: string): string { + // Utiliser DOMPurify pour une sanitisation robuste et Ă©prouvĂ©e + if (typeof window !== 'undefined' && DOMPurify.isSupported) { + return DOMPurify.sanitize(message, { + ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'i', 'b', 'span', 'a'], + ALLOWED_ATTR: ['class', 'href', 'title', 'target'], + ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|data):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i, + KEEP_CONTENT: true, + RETURN_DOM: false, + RETURN_DOM_FRAGMENT: false, + RETURN_TRUSTED_TYPE: false, + }); + } + + // Fallback sur la sanitisation manuelle si DOMPurify n'est pas disponible (SSR) const chatOptions: SanitizeOptions = { allowedTags: ['p', 'br', 'strong', 'em', 'u', 'i', 'b', 'span'], allowedAttributes: { diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index ada75e5e2..be575cf57 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -59,20 +59,36 @@ export default defineConfig(({ mode }) => { port: 3000, host: true, headers: { - 'Content-Security-Policy': [ - "default-src 'self'", - "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:", - "worker-src 'self' blob:", - "style-src 'self' 'unsafe-inline'", - "img-src 'self' data: https: blob:", - "connect-src 'self' ws: wss: http: https:", - "font-src 'self' data:", - "object-src 'none'", - "base-uri 'self'", - "form-action 'self'", - "frame-ancestors 'none'", - "upgrade-insecure-requests" - ].join('; ') + 'Content-Security-Policy': (() => { + const basePolicy = [ + "default-src 'self'", + "worker-src 'self' blob:", + "img-src 'self' data: https: blob:", + "connect-src 'self' ws: wss: http: https:", + "font-src 'self' data:", + "object-src 'none'", + "base-uri 'self'", + "form-action 'self'", + "frame-ancestors 'none'", + "upgrade-insecure-requests" + ]; + + // Production: Remove unsafe-inline and unsafe-eval, use nonces + // Dev: Keep unsafe-inline for Vite HMR, but remove unsafe-eval + if (isProduction) { + basePolicy.push( + "script-src 'self' 'nonce-__CSP_NONCE__' blob:", + "style-src 'self' 'nonce-__CSP_NONCE__'" + ); + } else { + basePolicy.push( + "script-src 'self' 'unsafe-inline' blob:", + "style-src 'self' 'unsafe-inline'" + ); + } + + return basePolicy.join('; '); + })() } }, build: { diff --git a/e2e/example.spec.ts b/e2e/example.spec.ts new file mode 100644 index 000000000..54a906a4e --- /dev/null +++ b/e2e/example.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from '@playwright/test'; + +test('has title', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Playwright/); +}); + +test('get started link', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Click the get started link. + await page.getByRole('link', { name: 'Get started' }).click(); + + // Expects page to have a heading with the name of Installation. + await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); +}); diff --git a/e2e/test-1.spec.ts b/e2e/test-1.spec.ts new file mode 100644 index 000000000..fffb6026e --- /dev/null +++ b/e2e/test-1.spec.ts @@ -0,0 +1,15 @@ +import { test, expect } from '@playwright/test'; + +test('test', async ({ page }) => { + await page.goto('http://localhost:3000/login'); + await page.getByRole('link', { name: 'S\'inscrire' }).click(); + await page.locator('input[name="email"]').click(); + await page.locator('input[name="email"]').fill('test@free.fr'); + await page.locator('input[name="email"]').press('Tab'); + await page.locator('input[name="username"]').fill('tester'); + await page.locator('input[name="username"]').press('Tab'); + await page.locator('input[name="password"]').fill('Test12345678!'); + await page.locator('input[name="password_confirm"]').click(); + await page.locator('input[name="password_confirm"]').fill('Test12345678!'); + await page.getByRole('button', { name: 'S\'inscrire' }).click(); +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..6e152f425 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3506 @@ +{ + "name": "veza", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@eslint/js": "^9.39.1", + "@playwright/test": "^1.57.0", + "@types/node": "^25.0.3", + "eslint": "^9.39.1", + "eslint-plugin-react": "^7.37.5", + "globals": "^16.5.0", + "prettier": "3.6.2", + "typescript": "^5.9.3", + "typescript-eslint": "^8.46.3" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@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/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "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": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz", + "integrity": "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.50.0", + "@typescript-eslint/type-utils": "8.50.0", + "@typescript-eslint/utils": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.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.50.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz", + "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.50.0", + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/typescript-estree": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.0.tgz", + "integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.50.0", + "@typescript-eslint/types": "^8.50.0", + "debug": "^4.3.4" + }, + "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.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz", + "integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz", + "integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==", + "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.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz", + "integrity": "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/typescript-estree": "8.50.0", + "@typescript-eslint/utils": "8.50.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", + "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", + "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.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz", + "integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.50.0", + "@typescript-eslint/tsconfig-utils": "8.50.0", + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0", + "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz", + "integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.50.0", + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/typescript-estree": "8.50.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz", + "integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.50.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-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/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/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/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/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/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/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/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", + "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.1", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "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.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/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/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-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-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/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/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "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/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "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/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "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/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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.50.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.50.0.tgz", + "integrity": "sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.50.0", + "@typescript-eslint/parser": "8.50.0", + "@typescript-eslint/typescript-estree": "8.50.0", + "@typescript-eslint/utils": "8.50.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/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/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/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/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" + } + } + } +} diff --git a/package.json b/package.json index 03f2cbb72..6029c4c97 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,14 @@ { "devDependencies": { "@eslint/js": "^9.39.1", + "@playwright/test": "^1.57.0", + "@types/node": "^25.0.3", "eslint": "^9.39.1", "eslint-plugin-react": "^7.37.5", "globals": "^16.5.0", "prettier": "3.6.2", "typescript": "^5.9.3", "typescript-eslint": "^8.46.3" - } + }, + "scripts": {} } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..dfe72abfe --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,79 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './e2e', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('')`. */ + // baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://localhost:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/veza-backend-api/cmd/tools/create_test_user/main.go b/veza-backend-api/cmd/tools/create_test_user/main.go new file mode 100644 index 000000000..bbe1f9da5 --- /dev/null +++ b/veza-backend-api/cmd/tools/create_test_user/main.go @@ -0,0 +1,109 @@ +package main + +import ( + "fmt" + "log" + "os" + "strings" + + "github.com/joho/godotenv" + "golang.org/x/crypto/bcrypt" + "gorm.io/driver/postgres" + "gorm.io/gorm" + + "veza-backend-api/internal/models" +) + +func main() { + // Load .env file + if err := godotenv.Load(); err != nil { + log.Printf("Note: .env file not found, using system environment variables") + } + + // Get database connection string + databaseURL := os.Getenv("DATABASE_URL") + if databaseURL == "" { + // Fallback to individual components + dbHost := getEnv("DB_HOST", "localhost") + dbPort := getEnv("DB_PORT", "5432") + dbUser := getEnv("DB_USER", "veza") + dbPassword := getEnv("DB_PASSWORD", "password") + dbName := getEnv("DB_NAME", "veza") + databaseURL = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + dbHost, dbPort, dbUser, dbPassword, dbName) + } + + // Connect to database + db, err := gorm.Open(postgres.Open(databaseURL), &gorm.Config{}) + if err != nil { + log.Fatalf("Failed to connect to database: %v", err) + } + + // Get test user credentials from environment or use defaults + email := getEnv("TEST_EMAIL", "user@example.com") + password := getEnv("TEST_PASSWORD", "password123") + username := getEnv("TEST_USERNAME", "testuser") + + // Check if user already exists + var existingUser models.User + result := db.Where("email = ?", email).First(&existingUser) + if result.Error == nil { + log.Printf("User with email %s already exists (ID: %s)", email, existingUser.ID) + + // Update password if needed + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + log.Fatalf("Failed to hash password: %v", err) + } + + existingUser.PasswordHash = string(hashedPassword) + existingUser.IsVerified = true + existingUser.IsActive = true + + if err := db.Save(&existingUser).Error; err != nil { + log.Fatalf("Failed to update user: %v", err) + } + + log.Printf("✅ Updated existing user: %s (password reset, verified and active)", email) + return + } + + // Hash password + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + log.Fatalf("Failed to hash password: %v", err) + } + + // Generate slug from username + slug := strings.ToLower(strings.ReplaceAll(username, "_", "-")) + + // Create user + user := &models.User{ + Email: email, + Username: username, + Slug: slug, + PasswordHash: string(hashedPassword), + IsVerified: true, + IsActive: true, + Role: "user", + FirstName: "Test", + LastName: "User", + } + + if err := db.Create(user).Error; err != nil { + log.Fatalf("Failed to create user: %v", err) + } + + log.Printf("✅ Created test user successfully!") + log.Printf(" Email: %s", email) + log.Printf(" Username: %s", username) + log.Printf(" Password: %s", password) + log.Printf(" ID: %s", user.ID) +} + +func getEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return strings.TrimSpace(value) + } + return defaultValue +} diff --git a/veza-backend-api/migrations/000000_cleanup_refresh_tokens.sql b/veza-backend-api/migrations/000000_cleanup_refresh_tokens.sql deleted file mode 100644 index d3b53e537..000000000 --- a/veza-backend-api/migrations/000000_cleanup_refresh_tokens.sql +++ /dev/null @@ -1,18 +0,0 @@ --- Migration to cleanup refresh_tokens table --- Remove legacy column 'token' which caused NULL constraint violations --- Ensure correct constraints on token_hash - -BEGIN; - --- 1. Remove the legacy 'token' column which is no longer used by the application --- The application now uses 'token_hash' for secure storage -ALTER TABLE refresh_tokens DROP COLUMN IF EXISTS token; - --- 2. Ensure token_hash has the correct constraints --- It should be NOT NULL and UNIQUE to prevent duplicates and ensure integrity -ALTER TABLE refresh_tokens ALTER COLUMN token_hash SET NOT NULL; - --- 3. Add comment to clarify the column usage -COMMENT ON COLUMN refresh_tokens.token_hash IS 'SHA-256 hash of the refresh token. The raw token is never stored.'; - -COMMIT; diff --git a/veza-backend-api/migrations/011_cleanup_refresh_tokens.sql b/veza-backend-api/migrations/011_cleanup_refresh_tokens.sql new file mode 100644 index 000000000..2580075f9 --- /dev/null +++ b/veza-backend-api/migrations/011_cleanup_refresh_tokens.sql @@ -0,0 +1,49 @@ +-- Migration to cleanup refresh_tokens table +-- Remove legacy column 'token' which caused NULL constraint violations +-- Ensure correct constraints on token_hash +-- This migration runs AFTER 010_auth_and_users.sql which creates the refresh_tokens table + +BEGIN; + +-- Check if the table exists before attempting to alter it +DO $$ +BEGIN + -- Only proceed if the refresh_tokens table exists + IF EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = 'refresh_tokens' + ) THEN + -- 1. Remove the legacy 'token' column which is no longer used by the application + -- The application now uses 'token_hash' for secure storage + ALTER TABLE refresh_tokens DROP COLUMN IF EXISTS token; + + -- 2. Ensure token_hash has the correct constraints + -- It should be NOT NULL and UNIQUE to prevent duplicates and ensure integrity + -- Only set NOT NULL if the column exists and doesn't already have the constraint + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'refresh_tokens' + AND column_name = 'token_hash' + ) THEN + -- Check if column is already NOT NULL + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'refresh_tokens' + AND column_name = 'token_hash' + AND is_nullable = 'YES' + ) THEN + ALTER TABLE refresh_tokens ALTER COLUMN token_hash SET NOT NULL; + END IF; + END IF; + + -- 3. Add comment to clarify the column usage + COMMENT ON COLUMN refresh_tokens.token_hash IS 'SHA-256 hash of the refresh token. The raw token is never stored.'; + ELSE + RAISE NOTICE 'Table refresh_tokens does not exist yet. Skipping cleanup migration.'; + END IF; +END $$; + +COMMIT;