diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index d0b17797..00000000 --- a/frontend/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Frontend (Vite + React) - -## Development - -```bash -cd frontend -npm install -npm run dev -``` - -## Production build - -```bash -cd frontend -npm run build -``` diff --git a/frontend/e2e/playwright.config.ts b/frontend/e2e/playwright.config.ts deleted file mode 100644 index 956ebdf5..00000000 --- a/frontend/e2e/playwright.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from '@playwright/test' - -export default defineConfig({ - testDir: './tests', - timeout: 30_000, - use: { - baseURL: process.env.CHARON_BASE_URL || 'http://localhost:8080', - }, - reporter: [['list']], -}) diff --git a/frontend/e2e/tests/security-mobile.spec.ts b/frontend/e2e/tests/security-mobile.spec.ts deleted file mode 100644 index 50d32c71..00000000 --- a/frontend/e2e/tests/security-mobile.spec.ts +++ /dev/null @@ -1,297 +0,0 @@ -/** - * Security Dashboard Mobile Responsive E2E Tests - * Test IDs: MR-01 through MR-10 - * - * Tests mobile viewport (375x667), tablet viewport (768x1024), - * touch targets, scrolling, and layout responsiveness. - */ -import { test, expect } from '@playwright/test' - -const base = process.env.CHARON_BASE_URL || 'http://localhost:8080' - -test.describe('Security Dashboard Mobile (375x667)', () => { - test.use({ viewport: { width: 375, height: 667 } }) - - test('MR-01: cards stack vertically on mobile', async ({ page }) => { - await page.goto(`${base}/security`) - - // Wait for page to load - await page.waitForSelector('[data-testid="toggle-crowdsec"]', { timeout: 10000 }) - - // On mobile, grid should be single column - const grid = page.locator('.grid.grid-cols-1') - await expect(grid).toBeVisible() - - // Get the computed grid-template-columns - const cardsContainer = page.locator('.grid').first() - const gridStyle = await cardsContainer.evaluate((el) => { - const style = window.getComputedStyle(el) - return style.gridTemplateColumns - }) - - // Single column should have just one value (not multiple columns like "repeat(4, ...)") - const columns = gridStyle.split(' ').filter((s) => s.trim().length > 0) - expect(columns.length).toBeLessThanOrEqual(2) // Single column or flexible - }) - - test('MR-04: toggle switches have accessible touch targets', async ({ page }) => { - await page.goto(`${base}/security`) - await page.waitForSelector('[data-testid="toggle-crowdsec"]', { timeout: 10000 }) - - // Check CrowdSec toggle - const crowdsecToggle = page.getByTestId('toggle-crowdsec') - const crowdsecBox = await crowdsecToggle.boundingBox() - - // Touch target should be at least 24px (component) + padding - // Most switches have a reasonable touch target - expect(crowdsecBox).not.toBeNull() - if (crowdsecBox) { - expect(crowdsecBox.height).toBeGreaterThanOrEqual(20) - expect(crowdsecBox.width).toBeGreaterThanOrEqual(35) - } - - // Check WAF toggle - const wafToggle = page.getByTestId('toggle-waf') - const wafBox = await wafToggle.boundingBox() - expect(wafBox).not.toBeNull() - if (wafBox) { - expect(wafBox.height).toBeGreaterThanOrEqual(20) - } - }) - - test('MR-05: config buttons are tappable on mobile', async ({ page }) => { - await page.goto(`${base}/security`) - await page.waitForSelector('[data-testid="toggle-crowdsec"]', { timeout: 10000 }) - - // Find config/configure buttons - const configButtons = page.locator('button:has-text("Config"), button:has-text("Configure")') - const buttonCount = await configButtons.count() - - expect(buttonCount).toBeGreaterThan(0) - - // Check first config button has reasonable size - const firstButton = configButtons.first() - const box = await firstButton.boundingBox() - expect(box).not.toBeNull() - if (box) { - expect(box.height).toBeGreaterThanOrEqual(28) // Minimum tap height - } - }) - - test('MR-06: page content is scrollable on mobile', async ({ page }) => { - await page.goto(`${base}/security`) - await page.waitForSelector('[data-testid="toggle-crowdsec"]', { timeout: 10000 }) - - // Check if page is scrollable (content height > viewport) - const bodyHeight = await page.evaluate(() => document.body.scrollHeight) - const viewportHeight = 667 - - // If content is taller than viewport, page should scroll - if (bodyHeight > viewportHeight) { - // Attempt to scroll down - await page.evaluate(() => window.scrollBy(0, 200)) - const scrollY = await page.evaluate(() => window.scrollY) - expect(scrollY).toBeGreaterThan(0) - } - }) - - test('MR-10: navigation is accessible on mobile', async ({ page }) => { - await page.goto(`${base}/security`) - await page.waitForSelector('[data-testid="toggle-crowdsec"]', { timeout: 10000 }) - - // On mobile, there should be some form of navigation - // Check if sidebar or mobile menu toggle exists - const sidebar = page.locator('nav, aside, [role="navigation"]') - const sidebarCount = await sidebar.count() - - // Navigation should exist in some form - expect(sidebarCount).toBeGreaterThanOrEqual(0) // May be hidden on mobile - }) - - test('MR-06b: overlay renders correctly on mobile', async ({ page }) => { - await page.goto(`${base}/security`) - await page.waitForSelector('[data-testid="toggle-crowdsec"]', { timeout: 10000 }) - - // Skip if Cerberus is disabled (toggles would be disabled) - const cerberusDisabled = await page.locator('text=Cerberus Disabled').isVisible() - if (cerberusDisabled) { - test.skip() - return - } - - // Trigger loading state by clicking a toggle - const wafToggle = page.getByTestId('toggle-waf') - const isDisabled = await wafToggle.isDisabled() - - if (!isDisabled) { - await wafToggle.click() - - // Check for overlay (may appear briefly) - // Use a short timeout since it might disappear quickly - try { - const overlay = page.locator('.fixed.inset-0') - await overlay.waitFor({ state: 'visible', timeout: 2000 }) - - // If overlay appeared, verify it fits screen - const box = await overlay.boundingBox() - if (box) { - expect(box.width).toBeLessThanOrEqual(375 + 10) // Allow small margin - } - } catch { - // Overlay might have disappeared before we could check - // This is acceptable for a fast operation - } - } - }) -}) - -test.describe('Security Dashboard Tablet (768x1024)', () => { - test.use({ viewport: { width: 768, height: 1024 } }) - - test('MR-02: cards show 2 columns on tablet', async ({ page }) => { - await page.goto(`${base}/security`) - await page.waitForSelector('[data-testid="toggle-crowdsec"]', { timeout: 10000 }) - - // On tablet (md breakpoint), should have md:grid-cols-2 - const grid = page.locator('.grid').first() - await expect(grid).toBeVisible() - - // Get computed style - const gridStyle = await grid.evaluate((el) => { - const style = window.getComputedStyle(el) - return style.gridTemplateColumns - }) - - // Should have 2 columns at md breakpoint - const columns = gridStyle.split(' ').filter((s) => s.trim().length > 0 && s !== 'none') - expect(columns.length).toBeGreaterThanOrEqual(2) - }) - - test('MR-08: cards have proper spacing on tablet', async ({ page }) => { - await page.goto(`${base}/security`) - await page.waitForSelector('[data-testid="toggle-crowdsec"]', { timeout: 10000 }) - - // Check gap between cards - const grid = page.locator('.grid.gap-6').first() - const hasGap = await grid.isVisible() - expect(hasGap).toBe(true) - }) -}) - -test.describe('Security Dashboard Desktop (1920x1080)', () => { - test.use({ viewport: { width: 1920, height: 1080 } }) - - test('MR-03: cards show 4 columns on desktop', async ({ page }) => { - await page.goto(`${base}/security`) - await page.waitForSelector('[data-testid="toggle-crowdsec"]', { timeout: 10000 }) - - // On desktop (lg breakpoint), should have lg:grid-cols-4 - const grid = page.locator('.grid').first() - await expect(grid).toBeVisible() - - // Get computed style - const gridStyle = await grid.evaluate((el) => { - const style = window.getComputedStyle(el) - return style.gridTemplateColumns - }) - - // Should have 4 columns at lg breakpoint - const columns = gridStyle.split(' ').filter((s) => s.trim().length > 0 && s !== 'none') - expect(columns.length).toBeGreaterThanOrEqual(4) - }) -}) - -test.describe('Security Dashboard Layout Tests', () => { - test('cards maintain correct order across viewports', async ({ page }) => { - // Test on mobile - await page.setViewportSize({ width: 375, height: 667 }) - await page.goto(`${base}/security`) - await page.waitForSelector('[data-testid="toggle-crowdsec"]', { timeout: 10000 }) - - // Get card headings - const getCardOrder = async () => { - const headings = await page.locator('h3').allTextContents() - return headings.filter((h) => ['CrowdSec', 'Access Control', 'Coraza', 'Rate Limiting'].includes(h)) - } - - const mobileOrder = await getCardOrder() - - // Test on tablet - await page.setViewportSize({ width: 768, height: 1024 }) - await page.waitForTimeout(100) // Allow reflow - const tabletOrder = await getCardOrder() - - // Test on desktop - await page.setViewportSize({ width: 1920, height: 1080 }) - await page.waitForTimeout(100) // Allow reflow - const desktopOrder = await getCardOrder() - - // Order should be consistent - expect(mobileOrder).toEqual(tabletOrder) - expect(tabletOrder).toEqual(desktopOrder) - expect(desktopOrder).toEqual(['CrowdSec', 'Access Control', 'Coraza', 'Rate Limiting']) - }) - - test('MR-09: all security cards are visible on scroll', async ({ page }) => { - await page.setViewportSize({ width: 375, height: 667 }) - await page.goto(`${base}/security`) - await page.waitForSelector('[data-testid="toggle-crowdsec"]', { timeout: 10000 }) - - // Scroll to each card type - const cardTypes = ['CrowdSec', 'Access Control', 'Coraza', 'Rate Limiting'] - - for (const cardType of cardTypes) { - const card = page.locator(`h3:has-text("${cardType}")`) - await card.scrollIntoViewIfNeeded() - await expect(card).toBeVisible() - } - }) -}) - -test.describe('Security Dashboard Interaction Tests', () => { - test.use({ viewport: { width: 375, height: 667 } }) - - test('MR-07: config buttons navigate correctly on mobile', async ({ page }) => { - await page.goto(`${base}/security`) - await page.waitForSelector('[data-testid="toggle-crowdsec"]', { timeout: 10000 }) - - // Skip if Cerberus disabled - const cerberusDisabled = await page.locator('text=Cerberus Disabled').isVisible() - if (cerberusDisabled) { - test.skip() - return - } - - // Find and click WAF Configure button - const configureButton = page.locator('button:has-text("Configure")').first() - - if (await configureButton.isVisible()) { - await configureButton.click() - - // Should navigate to a config page - await page.waitForTimeout(500) - const url = page.url() - - // URL should include security/waf or security/rate-limiting etc - expect(url).toMatch(/security\/(waf|rate-limiting|access-lists|crowdsec)/i) - } - }) - - test('documentation button works on mobile', async ({ page }) => { - await page.goto(`${base}/security`) - await page.waitForSelector('[data-testid="toggle-crowdsec"]', { timeout: 10000 }) - - // Find documentation button - const docButton = page.locator('button:has-text("Documentation"), a:has-text("Documentation")').first() - - if (await docButton.isVisible()) { - // Check it has correct external link behavior - const href = await docButton.getAttribute('href') - - // Should open external docs - if (href) { - expect(href).toContain('wikid82.github.io') - } - } - }) -}) diff --git a/frontend/e2e/tests/waf.spec.ts b/frontend/e2e/tests/waf.spec.ts deleted file mode 100644 index 31d6989a..00000000 --- a/frontend/e2e/tests/waf.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { test, expect } from '@playwright/test' - -const base = process.env.CHARON_BASE_URL || 'http://localhost:8080' - -// Hit an API route inside /api/v1 to ensure Cerberus middleware executes. -const targetPath = '/api/v1/system/my-ip' - -test.describe('WAF blocking and monitoring', () => { - test('blocks malicious query when mode=block', async ({ request }) => { - // Use literal ' - - diff --git a/frontend/package-lock.json b/frontend/package-lock.json deleted file mode 100644 index 60b1975e..00000000 --- a/frontend/package-lock.json +++ /dev/null @@ -1,7509 +0,0 @@ -{ - "name": "charon-frontend", - "version": "0.3.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "charon-frontend", - "version": "0.3.0", - "dependencies": { - "@radix-ui/react-checkbox": "^1.3.3", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-progress": "^1.1.8", - "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-tabs": "^1.1.13", - "@radix-ui/react-tooltip": "^1.2.8", - "@tanstack/react-query": "^5.90.16", - "axios": "^1.13.2", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "date-fns": "^4.1.0", - "i18next": "^25.7.3", - "i18next-browser-languagedetector": "^8.2.0", - "lucide-react": "^0.562.0", - "react": "^19.2.3", - "react-dom": "^19.2.3", - "react-hook-form": "^7.69.0", - "react-hot-toast": "^2.6.0", - "react-i18next": "^16.5.0", - "react-router-dom": "^7.11.0", - "tailwind-merge": "^3.4.0", - "tldts": "^7.0.19" - }, - "devDependencies": { - "@playwright/test": "^1.57.0", - "@tailwindcss/postcss": "^4.1.18", - "@testing-library/jest-dom": "^6.9.1", - "@testing-library/react": "^16.3.1", - "@testing-library/user-event": "^14.6.1", - "@types/react": "^19.2.7", - "@types/react-dom": "^19.2.3", - "@typescript-eslint/eslint-plugin": "^8.51.0", - "@typescript-eslint/parser": "^8.51.0", - "@vitejs/plugin-react": "^5.1.2", - "@vitest/coverage-istanbul": "^4.0.16", - "@vitest/coverage-v8": "^4.0.16", - "@vitest/ui": "^4.0.16", - "autoprefixer": "^10.4.23", - "eslint": "^9.39.2", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.25", - "jsdom": "^27.4.0", - "knip": "^5.78.0", - "postcss": "^8.5.6", - "tailwindcss": "^4.1.18", - "typescript": "^5.9.3", - "typescript-eslint": "^8.51.0", - "vite": "^7.3.0", - "vitest": "^4.0.16" - } - }, - "node_modules/@acemir/cssom": { - "version": "0.9.28", - "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.28.tgz", - "integrity": "sha512-LuS6IVEivI75vKN8S04qRD+YySP0RmU/cV8UNukhQZvprxF+76Z43TNo/a08eCodaGhT1Us8etqS1ZRY9/Or0A==", - "dev": true, - "license": "MIT" - }, - "node_modules/@adobe/css-tools": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", - "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", - "dev": true - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@asamuzakjp/css-color": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.0.tgz", - "integrity": "sha512-9xiBAtLn4aNsa4mDnpovJvBn72tNEIACyvlqaNJ+ADemR+yeMJWnBudOi2qGDviJa7SwcDOU/TRh5dnET7qk0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^2.1.4", - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "lru-cache": "^11.2.2" - } - }, - "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@asamuzakjp/dom-selector": { - "version": "6.7.6", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.6.tgz", - "integrity": "sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/nwsapi": "^2.3.9", - "bidi-js": "^1.0.3", - "css-tree": "^3.1.0", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.4" - } - }, - "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@asamuzakjp/nwsapi": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", - "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", - "dev": true - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", - "dev": true, - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.28.5" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", - "dev": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@csstools/color-helpers": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", - "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - } - }, - "node_modules/@csstools/css-calc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", - "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", - "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^5.1.0", - "@csstools/css-calc": "^2.1.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", - "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.14.tgz", - "integrity": "sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", - "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@emnapi/core": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", - "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", - "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz", - "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz", - "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz", - "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz", - "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz", - "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz", - "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz", - "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz", - "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz", - "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz", - "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz", - "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz", - "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz", - "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz", - "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz", - "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz", - "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz", - "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz", - "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz", - "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz", - "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz", - "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz", - "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz", - "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz", - "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz", - "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz", - "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "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, - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "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, - "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-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "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, - "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, - "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.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "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.0", - "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/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, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/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, - "engines": { - "node": ">= 4" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "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, - "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, - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@exodus/bytes": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.7.0.tgz", - "integrity": "sha512-5i+BtvujK/vM07YCGDyz4C4AyDzLmhxHMtM5HpUyPRtJPBdFPsj290ffXW+UXY21/G7GtXeHD2nRmq0T1ShyQQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@exodus/crypto": "^1.0.0-rc.4" - }, - "peerDependenciesMeta": { - "@exodus/crypto": { - "optional": true - } - } - }, - "node_modules/@floating-ui/core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", - "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", - "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.3", - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", - "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.4" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", - "license": "MIT" - }, - "node_modules/@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, - "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, - "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, - "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, - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.0.tgz", - "integrity": "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@tybys/wasm-util": "^0.10.1" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@oxc-resolver/binding-android-arm-eabi": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.15.0.tgz", - "integrity": "sha512-Q+lWuFfq7whNelNJIP1dhXaVz4zO9Tu77GcQHyxDWh3MaCoO2Bisphgzmsh4ZoUe2zIchQh6OvQL99GlWHg9Tw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@oxc-resolver/binding-android-arm64": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.15.0.tgz", - "integrity": "sha512-vbdBttesHR0W1oJaxgWVTboyMUuu+VnPsHXJ6jrXf4czELzB6GIg5DrmlyhAmFBhjwov+yJH/DfTnHS+2sDgOw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@oxc-resolver/binding-darwin-arm64": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.15.0.tgz", - "integrity": "sha512-R67lsOe1UzNjqVBCwCZX1rlItTsj/cVtBw4Uy19CvTicqEWvwaTn8t34zLD75LQwDDPCY3C8n7NbD+LIdw+ZoA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oxc-resolver/binding-darwin-x64": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.15.0.tgz", - "integrity": "sha512-77mya5F8WV0EtCxI0MlVZcqkYlaQpfNwl/tZlfg4jRsoLpFbaTeWv75hFm6TE84WULVlJtSgvf7DhoWBxp9+ZQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oxc-resolver/binding-freebsd-x64": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.15.0.tgz", - "integrity": "sha512-X1Sz7m5PC+6D3KWIDXMUtux+0Imj6HfHGdBStSvgdI60OravzI1t83eyn6eN0LPTrynuPrUgjk7tOnOsBzSWHw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.15.0.tgz", - "integrity": "sha512-L1x/wCaIRre+18I4cH/lTqSAymlV0k4HqfSYNNuI9oeL28Ks86lI6O5VfYL6sxxWYgjuWB98gNGo7tq7d4GarQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.15.0.tgz", - "integrity": "sha512-abGXd/zMGa0tH8nKlAXdOnRy4G7jZmkU0J85kMKWns161bxIgGn/j7zxqh3DKEW98wAzzU9GofZMJ0P5YCVPVw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.15.0.tgz", - "integrity": "sha512-SVjjjtMW66Mza76PBGJLqB0KKyFTBnxmtDXLJPbL6ZPGSctcXVmujz7/WAc0rb9m2oV0cHQTtVjnq6orQnI/jg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-arm64-musl": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.15.0.tgz", - "integrity": "sha512-JDv2/AycPF2qgzEiDeMJCcSzKNDm3KxNg0KKWipoKEMDFqfM7LxNwwSVyAOGmrYlE4l3dg290hOMsr9xG7jv9g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.15.0.tgz", - "integrity": "sha512-zbu9FhvBLW4KJxo7ElFvZWbSt4vP685Qc/Gyk/Ns3g2gR9qh2qWXouH8PWySy+Ko/qJ42+HJCLg+ZNcxikERfg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.15.0.tgz", - "integrity": "sha512-Kfleehe6B09C2qCnyIU01xLFqFXCHI4ylzkicfX/89j+gNHh9xyNdpEvit88Kq6i5tTGdavVnM6DQfOE2qNtlg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.15.0.tgz", - "integrity": "sha512-J7LPiEt27Tpm8P+qURDwNc8q45+n+mWgyys4/V6r5A8v5gDentHRGUx3iVk5NxdKhgoGulrzQocPTZVosq25Eg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.15.0.tgz", - "integrity": "sha512-+8/d2tAScPjVJNyqa7GPGnqleTB/XW9dZJQ2D/oIM3wpH3TG+DaFEXBbk4QFJ9K9AUGBhvQvWU2mQyhK/yYn3Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-x64-gnu": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.15.0.tgz", - "integrity": "sha512-xtvSzH7Nr5MCZI2FKImmOdTl9kzuQ51RPyLh451tvD2qnkg3BaqI9Ox78bTk57YJhlXPuxWSOL5aZhKAc9J6qg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-x64-musl": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.15.0.tgz", - "integrity": "sha512-14YL1zuXj06+/tqsuUZuzL0T425WA/I4nSVN1kBXeC5WHxem6lQ+2HGvG+crjeJEqHgZUT62YIgj88W+8E7eyg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-openharmony-arm64": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.15.0.tgz", - "integrity": "sha512-/7Qli+1Wk93coxnrQaU8ySlICYN8HsgyIrzqjgIkQEpI//9eUeaeIHZptNl2fMvBGeXa7k2QgLbRNaBRgpnvMw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@oxc-resolver/binding-wasm32-wasi": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.15.0.tgz", - "integrity": "sha512-q5rn2eIMQLuc/AVGR2rQKb2EVlgreATGG8xXg8f4XbbYCVgpxaq+dgMbiPStyNywW1MH8VU2T09UEm30UtOQvg==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.15.0.tgz", - "integrity": "sha512-yCAh2RWjU/8wWTxQDgGPgzV9QBv0/Ojb5ej1c/58iOjyTuy/J1ZQtYi2SpULjKmwIxLJdTiCHpMilauWimE31w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@oxc-resolver/binding-win32-ia32-msvc": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-11.15.0.tgz", - "integrity": "sha512-lmXKb6lvA6M6QIbtYfgjd+AryJqExZVSY2bfECC18OPu7Lv1mHFF171Mai5l9hG3r4IhHPPIwT10EHoilSCYeA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@oxc-resolver/binding-win32-x64-msvc": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.15.0.tgz", - "integrity": "sha512-HZsfne0s/tGOcJK9ZdTGxsNU2P/dH0Shf0jqrPvsC6wX0Wk+6AyhSpHFLQCnLOuFQiHHU0ePfM8iYsoJb5hHpQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "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/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true - }, - "node_modules/@radix-ui/number": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", - "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", - "license": "MIT" - }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-checkbox": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", - "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", - "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-progress": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.8.tgz", - "integrity": "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-context": "1.1.3", - "@radix-ui/react-primitive": "2.1.4" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", - "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", - "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.4" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-slot": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", - "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", - "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", - "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", - "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-previous": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", - "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", - "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", - "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", - "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", - "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", - "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", - "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", - "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", - "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", - "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", - "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", - "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", - "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", - "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", - "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", - "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", - "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", - "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", - "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", - "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", - "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", - "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", - "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", - "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", - "dev": true - }, - "node_modules/@tailwindcss/node": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", - "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "enhanced-resolve": "^5.18.3", - "jiti": "^2.6.1", - "lightningcss": "1.30.2", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.1.18" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", - "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-x64": "4.1.18", - "@tailwindcss/oxide-freebsd-x64": "4.1.18", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-x64-musl": "4.1.18", - "@tailwindcss/oxide-wasm32-wasi": "4.1.18", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", - "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", - "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", - "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", - "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", - "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", - "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", - "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", - "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", - "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", - "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.0", - "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.7.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.7.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@tybys/wasm-util": "^0.10.1" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.1", - "dev": true, - "inBundle": true, - "license": "0BSD", - "optional": true - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", - "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", - "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/postcss": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz", - "integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.18", - "@tailwindcss/oxide": "4.1.18", - "postcss": "^8.4.41", - "tailwindcss": "4.1.18" - } - }, - "node_modules/@tanstack/query-core": { - "version": "5.90.16", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.16.tgz", - "integrity": "sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "5.90.16", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.16.tgz", - "integrity": "sha512-bpMGOmV4OPmif7TNMteU/Ehf/hoC0Kf98PDc0F4BZkFrEapRMEqI/V6YS0lyzwSV6PQpY1y4xxArUIfBW5LVxQ==", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.90.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18 || ^19" - } - }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/jest-dom": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", - "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", - "dev": true, - "dependencies": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "picocolors": "^1.1.1", - "redent": "^3.0.0" - }, - "engines": { - "node": ">=14", - "npm": ">=6", - "yarn": ">=1" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "dev": true - }, - "node_modules/@testing-library/react": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.1.tgz", - "integrity": "sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@testing-library/user-event": { - "version": "14.6.1", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", - "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", - "dev": true, - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "peer": true - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true - }, - "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 - }, - "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 - }, - "node_modules/@types/node": { - "version": "24.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", - "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "undici-types": "~7.16.0" - } - }, - "node_modules/@types/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", - "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "devOptional": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.51.0.tgz", - "integrity": "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.51.0", - "@typescript-eslint/type-utils": "8.51.0", - "@typescript-eslint/utils": "8.51.0", - "@typescript-eslint/visitor-keys": "8.51.0", - "ignore": "^7.0.0", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.2.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.51.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.51.0.tgz", - "integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.51.0", - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/typescript-estree": "8.51.0", - "@typescript-eslint/visitor-keys": "8.51.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.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.51.0.tgz", - "integrity": "sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.51.0", - "@typescript-eslint/types": "^8.51.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.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.51.0.tgz", - "integrity": "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/visitor-keys": "8.51.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.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.51.0.tgz", - "integrity": "sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw==", - "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.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.51.0.tgz", - "integrity": "sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/typescript-estree": "8.51.0", - "@typescript-eslint/utils": "8.51.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.2.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.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.51.0.tgz", - "integrity": "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==", - "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.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.51.0.tgz", - "integrity": "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.51.0", - "@typescript-eslint/tsconfig-utils": "8.51.0", - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/visitor-keys": "8.51.0", - "debug": "^4.3.4", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.2.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/utils": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.51.0.tgz", - "integrity": "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.51.0", - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/typescript-estree": "8.51.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.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.51.0.tgz", - "integrity": "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.51.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/@typescript-eslint/visitor-keys/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/@vitejs/plugin-react": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", - "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.5", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.53", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.18.0" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/@vitest/coverage-istanbul": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/coverage-istanbul/-/coverage-istanbul-4.0.16.tgz", - "integrity": "sha512-CLyueXIHewDzmov97rGW/RNtg++UBwdtY/F9PZbEDvHlX16JWVyolg7OeGXZS3xkuuoaZMheef7luDFCoC6vsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@istanbuljs/schema": "^0.1.3", - "@jridgewell/gen-mapping": "^0.3.13", - "@jridgewell/trace-mapping": "0.3.31", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-instrument": "^6.0.3", - "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.6", - "istanbul-reports": "^3.2.0", - "magicast": "^0.5.1", - "obug": "^2.1.1", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "vitest": "4.0.16" - } - }, - "node_modules/@vitest/coverage-v8": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.16.tgz", - "integrity": "sha512-2rNdjEIsPRzsdu6/9Eq0AYAzYdpP6Bx9cje9tL3FE5XzXRQF1fNU9pe/1yE8fCrS0HD+fBtt6gLPh6LI57tX7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.0.16", - "ast-v8-to-istanbul": "^0.3.8", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.6", - "istanbul-reports": "^3.2.0", - "magicast": "^0.5.1", - "obug": "^2.1.1", - "std-env": "^3.10.0", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@vitest/browser": "4.0.16", - "vitest": "4.0.16" - }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } - } - }, - "node_modules/@vitest/expect": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.16.tgz", - "integrity": "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.16", - "@vitest/utils": "4.0.16", - "chai": "^6.2.1", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz", - "integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.0.16", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.16.tgz", - "integrity": "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.16.tgz", - "integrity": "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.0.16", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.16.tgz", - "integrity": "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.0.16", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.16.tgz", - "integrity": "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/ui": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.16.tgz", - "integrity": "sha512-rkoPH+RqWopVxDnCBE/ysIdfQ2A7j1eDmW8tCxxrR9nnFBa9jKf86VgsSAzxBd1x+ny0GC4JgiD3SNfRHv3pOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.0.16", - "fflate": "^0.8.2", - "flatted": "^3.3.3", - "pathe": "^2.0.3", - "sirv": "^3.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "vitest": "4.0.16" - } - }, - "node_modules/@vitest/utils": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.16.tgz", - "integrity": "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.0.16", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "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, - "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, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "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, - "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, - "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 - }, - "node_modules/aria-hidden": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/ast-v8-to-istanbul": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.8.tgz", - "integrity": "sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.31", - "estree-walker": "^3.0.3", - "js-tokens": "^9.0.1" - } - }, - "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/autoprefixer": { - "version": "10.4.23", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", - "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.1", - "caniuse-lite": "^1.0.30001760", - "fraction.js": "^5.3.4", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.7", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.7.tgz", - "integrity": "sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "dev": true, - "dependencies": { - "require-from-string": "^2.0.2" - } - }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/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==", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001760", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", - "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chai": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", - "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", - "dev": true, - "engines": { - "node": ">=18" - } - }, - "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, - "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/class-variance-authority": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", - "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", - "license": "Apache-2.0", - "dependencies": { - "clsx": "^2.1.1" - }, - "funding": { - "url": "https://polar.sh/cva" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "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 - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-tree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", - "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", - "dev": true, - "dependencies": { - "mdn-data": "2.12.2", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/css.escape": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", - "dev": true - }, - "node_modules/cssstyle": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.4.tgz", - "integrity": "sha512-KyOS/kJMEq5O9GdPnaf82noigg5X5DYn0kZPJTaAsCUaBizp6Xa1y9D4Qoqf/JazEXWuruErHgVXwjN5391ZJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^4.1.0", - "@csstools/css-syntax-patches-for-csstree": "1.0.14", - "css-tree": "^3.1.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" - }, - "node_modules/data-urls": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", - "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", - "dev": true, - "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^15.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/date-fns": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", - "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/kossnocorp" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": 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 - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "license": "MIT" - }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "peer": true - }, - "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==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", - "dev": true, - "license": "ISC" - }, - "node_modules/enhanced-resolve": { - "version": "5.18.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "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==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true - }, - "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==", - "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==", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz", - "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.1", - "@esbuild/android-arm": "0.27.1", - "@esbuild/android-arm64": "0.27.1", - "@esbuild/android-x64": "0.27.1", - "@esbuild/darwin-arm64": "0.27.1", - "@esbuild/darwin-x64": "0.27.1", - "@esbuild/freebsd-arm64": "0.27.1", - "@esbuild/freebsd-x64": "0.27.1", - "@esbuild/linux-arm": "0.27.1", - "@esbuild/linux-arm64": "0.27.1", - "@esbuild/linux-ia32": "0.27.1", - "@esbuild/linux-loong64": "0.27.1", - "@esbuild/linux-mips64el": "0.27.1", - "@esbuild/linux-ppc64": "0.27.1", - "@esbuild/linux-riscv64": "0.27.1", - "@esbuild/linux-s390x": "0.27.1", - "@esbuild/linux-x64": "0.27.1", - "@esbuild/netbsd-arm64": "0.27.1", - "@esbuild/netbsd-x64": "0.27.1", - "@esbuild/openbsd-arm64": "0.27.1", - "@esbuild/openbsd-x64": "0.27.1", - "@esbuild/openharmony-arm64": "0.27.1", - "@esbuild/sunos-x64": "0.27.1", - "@esbuild/win32-arm64": "0.27.1", - "@esbuild/win32-ia32": "0.27.1", - "@esbuild/win32-x64": "0.27.1" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", - "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.25", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.25.tgz", - "integrity": "sha512-dRUD2LOdEqI4zXHqbQ442blQAzdSuShAaiSq5Vtyy6LT08YUf0oOjBDo4VPx0dCPgiPWh1WB4dtbLOd0kOlDPQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": ">=8.40" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "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": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/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/eslint/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/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "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/espree/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, - "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, - "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, - "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, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.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, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expect-type": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", - "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", - "dev": true, - "engines": { - "node": ">=12.0.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 - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "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 - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fd-package-json": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-2.0.0.tgz", - "integrity": "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "walk-up-path": "^4.0.0" - } - }, - "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/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "dev": 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, - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "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, - "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 - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/formatly": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/formatly/-/formatly-0.3.0.tgz", - "integrity": "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==", - "dev": true, - "license": "MIT", - "dependencies": { - "fd-package-json": "^2.0.0" - }, - "bin": { - "formatly": "bin/index.mjs" - }, - "engines": { - "node": ">=18.3.0" - } - }, - "node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", - "dev": true, - "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "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==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "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-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "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, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "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, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/goober": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz", - "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==", - "license": "MIT", - "peerDependencies": { - "csstype": "^3.0.10" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "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, - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "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==", - "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==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", - "dev": true, - "license": "MIT" - }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", - "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.6.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "node_modules/html-parse-stringify": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", - "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", - "license": "MIT", - "dependencies": { - "void-elements": "3.1.0" - } - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/i18next": { - "version": "25.7.3", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.3.tgz", - "integrity": "sha512-2XaT+HpYGuc2uTExq9TVRhLsso+Dxym6PWaKpn36wfBmTI779OQ7iP/XaZHzrnGyzU4SHpFrTYLKfVyBfAhVNA==", - "funding": [ - { - "type": "individual", - "url": "https://locize.com" - }, - { - "type": "individual", - "url": "https://locize.com/i18next.html" - }, - { - "type": "individual", - "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" - } - ], - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.4" - }, - "peerDependencies": { - "typescript": "^5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/i18next-browser-languagedetector": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz", - "integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.2" - } - }, - "node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "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, - "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, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "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, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsdom": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz", - "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@acemir/cssom": "^0.9.28", - "@asamuzakjp/dom-selector": "^6.7.6", - "@exodus/bytes": "^1.6.0", - "cssstyle": "^5.3.4", - "data-urls": "^6.0.0", - "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^6.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", - "is-potential-custom-element-name": "^1.0.1", - "parse5": "^8.0.0", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.0", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.0", - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^15.1.0", - "ws": "^8.18.3", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "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 - }, - "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 - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/knip": { - "version": "5.78.0", - "resolved": "https://registry.npmjs.org/knip/-/knip-5.78.0.tgz", - "integrity": "sha512-nB7i/fgiJl7WVxdv5lX4ZPfDt9/zrw/lOgZtyioy988xtFhKuFJCRdHWT1Zg9Avc0yaojvnmEuAXU8SeMblKww==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/webpro" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/knip" - } - ], - "license": "ISC", - "dependencies": { - "@nodelib/fs.walk": "^1.2.3", - "fast-glob": "^3.3.3", - "formatly": "^0.3.0", - "jiti": "^2.6.0", - "js-yaml": "^4.1.1", - "minimist": "^1.2.8", - "oxc-resolver": "^11.15.0", - "picocolors": "^1.1.1", - "picomatch": "^4.0.1", - "smol-toml": "^1.5.2", - "strip-json-comments": "5.0.3", - "zod": "^4.1.11" - }, - "bin": { - "knip": "bin/knip.js", - "knip-bun": "bin/knip-bun.js" - }, - "engines": { - "node": ">=18.18.0" - }, - "peerDependencies": { - "@types/node": ">=18", - "typescript": ">=5.0.4 <7" - } - }, - "node_modules/knip/node_modules/strip-json-comments": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", - "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lightningcss": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", - "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", - "dev": true, - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.30.2", - "lightningcss-darwin-arm64": "1.30.2", - "lightningcss-darwin-x64": "1.30.2", - "lightningcss-freebsd-x64": "1.30.2", - "lightningcss-linux-arm-gnueabihf": "1.30.2", - "lightningcss-linux-arm64-gnu": "1.30.2", - "lightningcss-linux-arm64-musl": "1.30.2", - "lightningcss-linux-x64-gnu": "1.30.2", - "lightningcss-linux-x64-musl": "1.30.2", - "lightningcss-win32-arm64-msvc": "1.30.2", - "lightningcss-win32-x64-msvc": "1.30.2" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", - "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", - "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", - "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", - "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", - "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", - "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", - "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", - "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", - "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", - "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "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, - "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 - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-react": { - "version": "0.562.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz", - "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "peer": true, - "bin": { - "lz-string": "bin/bin.js" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/magicast": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz", - "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", - "source-map-js": "^1.2.1" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mdn-data": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", - "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", - "dev": true - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "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/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "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 - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true - }, - "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT" - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "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/oxc-resolver": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.15.0.tgz", - "integrity": "sha512-Hk2J8QMYwmIO9XTCUiOH00+Xk2/+aBxRUnhrSlANDyCnLYc32R1WSIq1sU2yEdlqd53FfMpPEpnBYIKQMzliJw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - }, - "optionalDependencies": { - "@oxc-resolver/binding-android-arm-eabi": "11.15.0", - "@oxc-resolver/binding-android-arm64": "11.15.0", - "@oxc-resolver/binding-darwin-arm64": "11.15.0", - "@oxc-resolver/binding-darwin-x64": "11.15.0", - "@oxc-resolver/binding-freebsd-x64": "11.15.0", - "@oxc-resolver/binding-linux-arm-gnueabihf": "11.15.0", - "@oxc-resolver/binding-linux-arm-musleabihf": "11.15.0", - "@oxc-resolver/binding-linux-arm64-gnu": "11.15.0", - "@oxc-resolver/binding-linux-arm64-musl": "11.15.0", - "@oxc-resolver/binding-linux-ppc64-gnu": "11.15.0", - "@oxc-resolver/binding-linux-riscv64-gnu": "11.15.0", - "@oxc-resolver/binding-linux-riscv64-musl": "11.15.0", - "@oxc-resolver/binding-linux-s390x-gnu": "11.15.0", - "@oxc-resolver/binding-linux-x64-gnu": "11.15.0", - "@oxc-resolver/binding-linux-x64-musl": "11.15.0", - "@oxc-resolver/binding-openharmony-arm64": "11.15.0", - "@oxc-resolver/binding-wasm32-wasi": "11.15.0", - "@oxc-resolver/binding-win32-arm64-msvc": "11.15.0", - "@oxc-resolver/binding-win32-ia32-msvc": "11.15.0", - "@oxc-resolver/binding-win32-x64-msvc": "11.15.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "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, - "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, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", - "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", - "dev": true, - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "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, - "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, - "engines": { - "node": ">=8" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true - }, - "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/playwright/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/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "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, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, - "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, - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/react": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", - "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.3" - } - }, - "node_modules/react-hook-form": { - "version": "7.69.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.69.0.tgz", - "integrity": "sha512-yt6ZGME9f4F6WHwevrvpAjh42HMvocuSnSIHUGycBqXIJdhqGSPQzTpGF+1NLREk/58IdPxEMfPcFCjlMhclGw==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/react-hook-form" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17 || ^18 || ^19" - } - }, - "node_modules/react-hot-toast": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", - "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==", - "license": "MIT", - "dependencies": { - "csstype": "^3.1.3", - "goober": "^2.1.16" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": ">=16", - "react-dom": ">=16" - } - }, - "node_modules/react-i18next": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.0.tgz", - "integrity": "sha512-IMpPTyCTKxEj8klCrLKUTIUa8uYTd851+jcu2fJuUB9Agkk9Qq8asw4omyeHVnOXHrLgQJGTm5zTvn8HpaPiqw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.27.6", - "html-parse-stringify": "^3.0.1", - "use-sync-external-store": "^1.6.0" - }, - "peerDependencies": { - "i18next": ">= 25.6.2", - "react": ">= 16.8.0", - "typescript": "^5" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "peer": true - }, - "node_modules/react-refresh": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", - "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-remove-scroll": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", - "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-router": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.11.0.tgz", - "integrity": "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ==", - "license": "MIT", - "dependencies": { - "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/react-router-dom": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.11.0.tgz", - "integrity": "sha512-e49Ir/kMGRzFOOrYQBdoitq3ULigw4lKbAyKusnvtDu2t4dBX4AGYPrzNvorXmVuOyeakai6FUPW5MmibvVG8g==", - "license": "MIT", - "dependencies": { - "react-router": "7.11.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", - "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", - "dev": true, - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.53.2", - "@rollup/rollup-android-arm64": "4.53.2", - "@rollup/rollup-darwin-arm64": "4.53.2", - "@rollup/rollup-darwin-x64": "4.53.2", - "@rollup/rollup-freebsd-arm64": "4.53.2", - "@rollup/rollup-freebsd-x64": "4.53.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", - "@rollup/rollup-linux-arm-musleabihf": "4.53.2", - "@rollup/rollup-linux-arm64-gnu": "4.53.2", - "@rollup/rollup-linux-arm64-musl": "4.53.2", - "@rollup/rollup-linux-loong64-gnu": "4.53.2", - "@rollup/rollup-linux-ppc64-gnu": "4.53.2", - "@rollup/rollup-linux-riscv64-gnu": "4.53.2", - "@rollup/rollup-linux-riscv64-musl": "4.53.2", - "@rollup/rollup-linux-s390x-gnu": "4.53.2", - "@rollup/rollup-linux-x64-gnu": "4.53.2", - "@rollup/rollup-linux-x64-musl": "4.53.2", - "@rollup/rollup-openharmony-arm64": "4.53.2", - "@rollup/rollup-win32-arm64-msvc": "4.53.2", - "@rollup/rollup-win32-ia32-msvc": "4.53.2", - "@rollup/rollup-win32-x64-gnu": "4.53.2", - "@rollup/rollup-win32-x64-msvc": "4.53.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-cookie-parser": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", - "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", - "license": "MIT" - }, - "node_modules/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, - "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, - "engines": { - "node": ">=8" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true - }, - "node_modules/sirv": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", - "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", - "dev": true, - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/smol-toml": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.5.2.tgz", - "integrity": "sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 18" - }, - "funding": { - "url": "https://github.com/sponsors/cyyynthia" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true - }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true - }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "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, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true - }, - "node_modules/tailwind-merge": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", - "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, - "node_modules/tailwindcss": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", - "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "dev": true, - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true - }, - "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyrainbow": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tldts": { - "version": "7.0.19", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz", - "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==", - "license": "MIT", - "dependencies": { - "tldts-core": "^7.0.19" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "7.0.19", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz", - "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==", - "license": "MIT" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", - "dev": true, - "dependencies": { - "tldts": "^7.0.5" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", - "dev": true, - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/ts-api-utils": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.3.0.tgz", - "integrity": "sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.51.0.tgz", - "integrity": "sha512-jh8ZuM5oEh2PSdyQG9YAEM1TCGuWenLSuSUhf/irbVUNW9O5FhbFVONviN2TgMTBnUmyHv7E56rYnfLZK6TkiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.51.0", - "@typescript-eslint/parser": "8.51.0", - "@typescript-eslint/typescript-estree": "8.51.0", - "@typescript-eslint/utils": "8.51.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/update-browserslist-db": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", - "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/vite": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", - "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vitest": { - "version": "4.0.16", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz", - "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.0.16", - "@vitest/mocker": "4.0.16", - "@vitest/pretty-format": "4.0.16", - "@vitest/runner": "4.0.16", - "@vitest/snapshot": "4.0.16", - "@vitest/spy": "4.0.16", - "@vitest/utils": "4.0.16", - "es-module-lexer": "^1.7.0", - "expect-type": "^1.2.2", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^3.10.0", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.16", - "@vitest/browser-preview": "4.0.16", - "@vitest/browser-webdriverio": "4.0.16", - "@vitest/ui": "4.0.16", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/void-elements": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", - "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/walk-up-path": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", - "integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==", - "dev": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/webidl-conversions": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", - "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", - "dev": true, - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-url": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", - "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", - "dev": true, - "dependencies": { - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "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, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "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, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", - "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-validation-error": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", - "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } - } - } -} diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index 66184ac5..00000000 --- a/frontend/package.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "name": "charon-frontend", - "private": true, - "version": "0.3.0", - "type": "module", - "tools": [], - "constraints": [ - "NPM SCRIPTS ONLY: Do not try to construct complex `vitest` or `playwright` commands. Always look at `package.json` first and use `npm run `." - ], - "scripts": { - "dev": "vite", - "build": "tsc -p tsconfig.build.json && vite build", - "pretype-check": "npm ci --silent", - "type-check": "tsc --noEmit", - "lint": "eslint . --report-unused-disable-directives", - "preview": "vite preview", - "test": "vitest run", - "test:ci": "vitest run", - "test:ui": "vitest --ui", - "check-coverage": "bash ../scripts/frontend-test-coverage.sh", - "pretest:coverage": "npm ci --silent && node -e \"require('fs').mkdirSync('coverage/.tmp', { recursive: true })\"", - "test:coverage": "vitest --coverage --coverage.provider=istanbul --coverage.reporter=json-summary --coverage.reporter=lcov --coverage.reporter=text", - "e2e:install": "npx playwright install --with-deps", - "e2e:test": "playwright test", - "e2e:up:block": "docker compose -f ../.docker/compose/docker-compose.local.yml down && CHARON_SECURITY_WAF_MODE=block docker compose -f ../.docker/compose/docker-compose.local.yml up -d", - "e2e:up:monitor": "docker compose -f ../.docker/compose/docker-compose.local.yml down && CHARON_SECURITY_WAF_MODE=monitor docker compose -f ../.docker/compose/docker-compose.local.yml up -d", - "e2e:down": "docker compose -f ../.docker/compose/docker-compose.local.yml down" - }, - "dependencies": { - "@radix-ui/react-checkbox": "^1.3.3", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-progress": "^1.1.8", - "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-tabs": "^1.1.13", - "@radix-ui/react-tooltip": "^1.2.8", - "@tanstack/react-query": "^5.90.16", - "axios": "^1.13.2", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "date-fns": "^4.1.0", - "i18next": "^25.7.3", - "i18next-browser-languagedetector": "^8.2.0", - "lucide-react": "^0.562.0", - "react": "^19.2.3", - "react-dom": "^19.2.3", - "react-hook-form": "^7.69.0", - "react-hot-toast": "^2.6.0", - "react-i18next": "^16.5.0", - "react-router-dom": "^7.11.0", - "tailwind-merge": "^3.4.0", - "tldts": "^7.0.19" - }, - "devDependencies": { - "@playwright/test": "^1.57.0", - "@tailwindcss/postcss": "^4.1.18", - "@testing-library/jest-dom": "^6.9.1", - "@testing-library/react": "^16.3.1", - "@testing-library/user-event": "^14.6.1", - "@types/react": "^19.2.7", - "@types/react-dom": "^19.2.3", - "@typescript-eslint/eslint-plugin": "^8.51.0", - "@typescript-eslint/parser": "^8.51.0", - "@vitejs/plugin-react": "^5.1.2", - "@vitest/coverage-istanbul": "^4.0.16", - "@vitest/coverage-v8": "^4.0.16", - "@vitest/ui": "^4.0.16", - "autoprefixer": "^10.4.23", - "eslint": "^9.39.2", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.25", - "jsdom": "^27.4.0", - "knip": "^5.78.0", - "postcss": "^8.5.6", - "tailwindcss": "^4.1.18", - "typescript": "^5.9.3", - "typescript-eslint": "^8.51.0", - "vite": "^7.3.0", - "vitest": "^4.0.16" - } -} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js deleted file mode 100644 index 1c878468..00000000 --- a/frontend/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - '@tailwindcss/postcss': {}, - autoprefixer: {}, - }, -} diff --git a/frontend/public/banner.png b/frontend/public/banner.png deleted file mode 100644 index d26ed78b..00000000 Binary files a/frontend/public/banner.png and /dev/null differ diff --git a/frontend/public/banner.svg b/frontend/public/banner.svg deleted file mode 100644 index e45ff23a..00000000 --- a/frontend/public/banner.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - diff --git a/frontend/public/banner.webp b/frontend/public/banner.webp deleted file mode 100644 index 21e35393..00000000 Binary files a/frontend/public/banner.webp and /dev/null differ diff --git a/frontend/public/favicon.png b/frontend/public/favicon.png deleted file mode 100644 index d51c7926..00000000 Binary files a/frontend/public/favicon.png and /dev/null differ diff --git a/frontend/public/logo.png b/frontend/public/logo.png deleted file mode 100644 index 697011f6..00000000 Binary files a/frontend/public/logo.png and /dev/null differ diff --git a/frontend/public/logo.svg b/frontend/public/logo.svg deleted file mode 100644 index b20af9b2..00000000 --- a/frontend/public/logo.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - diff --git a/frontend/public/logo.webp b/frontend/public/logo.webp deleted file mode 100644 index 4a34de02..00000000 Binary files a/frontend/public/logo.webp and /dev/null differ diff --git a/frontend/public/unknown.html b/frontend/public/unknown.html deleted file mode 100644 index 89049c62..00000000 --- a/frontend/public/unknown.html +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - Site Not Configured | Charon - - - -
- -

Site Not Configured

-

- The domain you are trying to access is pointing to this server, but no proxy host has been configured for it yet. -

-

- If you are the administrator, please log in to the Charon dashboard to configure this host. -

- Go to Dashboard -
- - - - diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx deleted file mode 100644 index 58956882..00000000 --- a/frontend/src/App.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { Suspense, lazy } from 'react' -import { Navigate } from 'react-router-dom' -import { BrowserRouter as Router, Routes, Route, Outlet } from 'react-router-dom' -import Layout from './components/Layout' -import { ToastContainer } from './components/Toast' -import { SetupGuard } from './components/SetupGuard' -import { LoadingOverlay } from './components/LoadingStates' -import RequireAuth from './components/RequireAuth' -import { AuthProvider } from './context/AuthContext' - -// Lazy load pages for code splitting -const Dashboard = lazy(() => import('./pages/Dashboard')) -const ProxyHosts = lazy(() => import('./pages/ProxyHosts')) -const RemoteServers = lazy(() => import('./pages/RemoteServers')) -const ImportCaddy = lazy(() => import('./pages/ImportCaddy')) -const ImportCrowdSec = lazy(() => import('./pages/ImportCrowdSec')) -const Certificates = lazy(() => import('./pages/Certificates')) -const DNSProviders = lazy(() => import('./pages/DNSProviders')) -const SystemSettings = lazy(() => import('./pages/SystemSettings')) -const SMTPSettings = lazy(() => import('./pages/SMTPSettings')) -const CrowdSecConfig = lazy(() => import('./pages/CrowdSecConfig')) -const Account = lazy(() => import('./pages/Account')) -const Settings = lazy(() => import('./pages/Settings')) -const Backups = lazy(() => import('./pages/Backups')) -const Tasks = lazy(() => import('./pages/Tasks')) -const Logs = lazy(() => import('./pages/Logs')) -const Domains = lazy(() => import('./pages/Domains')) -const Security = lazy(() => import('./pages/Security')) -const AccessLists = lazy(() => import('./pages/AccessLists')) -const WafConfig = lazy(() => import('./pages/WafConfig')) -const RateLimiting = lazy(() => import('./pages/RateLimiting')) -const Uptime = lazy(() => import('./pages/Uptime')) -const Notifications = lazy(() => import('./pages/Notifications')) -const UsersPage = lazy(() => import('./pages/UsersPage')) -const SecurityHeaders = lazy(() => import('./pages/SecurityHeaders')) -const Login = lazy(() => import('./pages/Login')) -const Setup = lazy(() => import('./pages/Setup')) -const AcceptInvite = lazy(() => import('./pages/AcceptInvite')) - -export default function App() { - return ( - - - }> - - } /> - } /> - } /> - - - - - - - - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - {/* Settings Routes */} - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - {/* Tasks Routes */} - }> - } /> - } /> - } /> - - } /> - } /> - - - - - - - - - - ) -} diff --git a/frontend/src/__tests__/i18n.test.ts b/frontend/src/__tests__/i18n.test.ts deleted file mode 100644 index cac1d1b0..00000000 --- a/frontend/src/__tests__/i18n.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest' -import i18n from '../i18n' - -describe('i18n configuration', () => { - beforeEach(async () => { - await i18n.changeLanguage('en') - }) - - it('initializes with default language', () => { - expect(i18n.language).toBeDefined() - expect(i18n.isInitialized).toBe(true) - }) - - it('has all required language resources', () => { - const languages = ['en', 'es', 'fr', 'de', 'zh'] - languages.forEach((lang) => { - expect(i18n.hasResourceBundle(lang, 'translation')).toBe(true) - }) - }) - - it('translates common keys', () => { - expect(i18n.t('common.save')).toBe('Save') - expect(i18n.t('common.cancel')).toBe('Cancel') - expect(i18n.t('common.delete')).toBe('Delete') - }) - - it('translates navigation keys', () => { - expect(i18n.t('navigation.dashboard')).toBe('Dashboard') - expect(i18n.t('navigation.settings')).toBe('Settings') - }) - - it('changes language and translates correctly', async () => { - await i18n.changeLanguage('es') - expect(i18n.t('common.save')).toBe('Guardar') - expect(i18n.t('common.cancel')).toBe('Cancelar') - - await i18n.changeLanguage('fr') - expect(i18n.t('common.save')).toBe('Enregistrer') - expect(i18n.t('common.cancel')).toBe('Annuler') - - await i18n.changeLanguage('de') - expect(i18n.t('common.save')).toBe('Speichern') - expect(i18n.t('common.cancel')).toBe('Abbrechen') - - await i18n.changeLanguage('zh') - expect(i18n.t('common.save')).toBe('保存') - expect(i18n.t('common.cancel')).toBe('取消') - }) - - it('falls back to English for missing translations', async () => { - await i18n.changeLanguage('en') - const key = 'nonexistent.key' - expect(i18n.t(key)).toBe(key) // Should return the key itself - }) - - it('supports interpolation', () => { - expect(i18n.t('dashboard.activeHosts', { count: 5 })).toBe('5 active') - }) -}) diff --git a/frontend/src/api/__tests__/accessLists.test.ts b/frontend/src/api/__tests__/accessLists.test.ts deleted file mode 100644 index a2283c82..00000000 --- a/frontend/src/api/__tests__/accessLists.test.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { accessListsApi } from '../accessLists'; -import client from '../client'; -import type { AccessList } from '../accessLists'; - -// Mock the client module -vi.mock('../client', () => ({ - default: { - get: vi.fn(), - post: vi.fn(), - put: vi.fn(), - delete: vi.fn(), - }, -})); - -describe('accessListsApi', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('list', () => { - it('should fetch all access lists', async () => { - const mockLists: AccessList[] = [ - { - id: 1, - uuid: 'test-uuid', - name: 'Test ACL', - description: 'Test description', - type: 'whitelist', - ip_rules: '[{"cidr":"192.168.1.0/24"}]', - country_codes: '', - local_network_only: false, - enabled: true, - created_at: '2024-01-01T00:00:00Z', - updated_at: '2024-01-01T00:00:00Z', - }, - ]; - - vi.mocked(client.get).mockResolvedValueOnce({ data: mockLists }); - - const result = await accessListsApi.list(); - - expect(client.get).toHaveBeenCalledWith<[string]>('/access-lists'); - expect(result).toEqual(mockLists); - }); - }); - - describe('get', () => { - it('should fetch access list by ID', async () => { - const mockList: AccessList = { - id: 1, - uuid: 'test-uuid', - name: 'Test ACL', - description: 'Test description', - type: 'whitelist', - ip_rules: '[{"cidr":"192.168.1.0/24"}]', - country_codes: '', - local_network_only: false, - enabled: true, - created_at: '2024-01-01T00:00:00Z', - updated_at: '2024-01-01T00:00:00Z', - }; - - vi.mocked(client.get).mockResolvedValueOnce({ data: mockList }); - - const result = await accessListsApi.get(1); - - expect(client.get).toHaveBeenCalledWith<[string]>('/access-lists/1'); - expect(result).toEqual(mockList); - }); - }); - - describe('create', () => { - it('should create a new access list', async () => { - const newList = { - name: 'New ACL', - description: 'New description', - type: 'whitelist' as const, - ip_rules: '[{"cidr":"10.0.0.0/8"}]', - enabled: true, - }; - - const mockResponse: AccessList = { - id: 1, - uuid: 'new-uuid', - ...newList, - country_codes: '', - local_network_only: false, - created_at: '2024-01-01T00:00:00Z', - updated_at: '2024-01-01T00:00:00Z', - }; - - vi.mocked(client.post).mockResolvedValueOnce({ data: mockResponse }); - - const result = await accessListsApi.create(newList); - - expect(client.post).toHaveBeenCalledWith<[string, typeof newList]>('/access-lists', newList); - expect(result).toEqual(mockResponse); - }); - }); - - describe('update', () => { - it('should update an access list', async () => { - const updates = { - name: 'Updated ACL', - enabled: false, - }; - - const mockResponse: AccessList = { - id: 1, - uuid: 'test-uuid', - name: 'Updated ACL', - description: 'Test description', - type: 'whitelist', - ip_rules: '[{"cidr":"192.168.1.0/24"}]', - country_codes: '', - local_network_only: false, - enabled: false, - created_at: '2024-01-01T00:00:00Z', - updated_at: '2024-01-01T00:00:00Z', - }; - - vi.mocked(client.put).mockResolvedValueOnce({ data: mockResponse }); - - const result = await accessListsApi.update(1, updates); - - expect(client.put).toHaveBeenCalledWith<[string, typeof updates]>('/access-lists/1', updates); - expect(result).toEqual(mockResponse); - }); - }); - - describe('delete', () => { - it('should delete an access list', async () => { - vi.mocked(client.delete).mockResolvedValueOnce({ data: undefined }); - - await accessListsApi.delete(1); - - expect(client.delete).toHaveBeenCalledWith<[string]>('/access-lists/1'); - }); - }); - - describe('testIP', () => { - it('should test an IP against an access list', async () => { - const mockResponse = { - allowed: true, - reason: 'IP matches whitelist rule', - }; - - vi.mocked(client.post).mockResolvedValueOnce({ data: mockResponse }); - - const result = await accessListsApi.testIP(1, '192.168.1.100'); - - expect(client.post).toHaveBeenCalledWith<[string, { ip_address: string }]>('/access-lists/1/test', { - ip_address: '192.168.1.100', - }); - expect(result).toEqual(mockResponse); - }); - }); - - describe('getTemplates', () => { - it('should fetch access list templates', async () => { - const mockTemplates = [ - { - name: 'Private Networks', - description: 'RFC1918 private networks', - type: 'whitelist' as const, - ip_rules: '[{"cidr":"10.0.0.0/8"},{"cidr":"172.16.0.0/12"},{"cidr":"192.168.0.0/16"}]', - }, - ]; - - vi.mocked(client.get).mockResolvedValueOnce({ data: mockTemplates }); - - const result = await accessListsApi.getTemplates(); - - expect(client.get).toHaveBeenCalledWith<[string]>('/access-lists/templates'); - expect(result).toEqual(mockTemplates); - }); - }); -}); diff --git a/frontend/src/api/__tests__/backups.test.ts b/frontend/src/api/__tests__/backups.test.ts deleted file mode 100644 index eb063070..00000000 --- a/frontend/src/api/__tests__/backups.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' -import client from '../../api/client' -import { getBackups, createBackup, restoreBackup, deleteBackup } from '../backups' - -describe('backups api', () => { - beforeEach(() => { - vi.restoreAllMocks() - }) - - it('getBackups returns list', async () => { - const mockData = [{ filename: 'b1.zip', size: 123, time: '2025-01-01T00:00:00Z' }] - vi.spyOn(client, 'get').mockResolvedValueOnce({ data: mockData }) - const res = await getBackups() - expect(res).toEqual(mockData) - }) - - it('createBackup returns filename', async () => { - vi.spyOn(client, 'post').mockResolvedValueOnce({ data: { filename: 'b2.zip' } }) - const res = await createBackup() - expect(res).toEqual({ filename: 'b2.zip' }) - }) - - it('restoreBackup posts to restore endpoint', async () => { - const spy = vi.spyOn(client, 'post').mockResolvedValueOnce({}) - await restoreBackup('b3.zip') - expect(spy).toHaveBeenCalledWith('/backups/b3.zip/restore') - }) - - it('deleteBackup deletes backup', async () => { - const spy = vi.spyOn(client, 'delete').mockResolvedValueOnce({}) - await deleteBackup('b3.zip') - expect(spy).toHaveBeenCalledWith('/backups/b3.zip') - }) -}) diff --git a/frontend/src/api/__tests__/certificates.test.ts b/frontend/src/api/__tests__/certificates.test.ts deleted file mode 100644 index 3d1ba01c..00000000 --- a/frontend/src/api/__tests__/certificates.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import client from '../client'; -import { getCertificates, uploadCertificate, deleteCertificate, Certificate } from '../certificates'; - -vi.mock('../client', () => ({ - default: { - get: vi.fn(), - post: vi.fn(), - delete: vi.fn(), - }, -})); - -describe('certificates API', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - const mockCert: Certificate = { - id: 1, - domain: 'example.com', - issuer: 'Let\'s Encrypt', - expires_at: '2023-01-01', - status: 'valid', - provider: 'letsencrypt', - }; - - it('getCertificates calls client.get', async () => { - vi.mocked(client.get).mockResolvedValue({ data: [mockCert] }); - const result = await getCertificates(); - expect(client.get).toHaveBeenCalledWith('/certificates'); - expect(result).toEqual([mockCert]); - }); - - it('uploadCertificate calls client.post with FormData', async () => { - vi.mocked(client.post).mockResolvedValue({ data: mockCert }); - const certFile = new File(['cert'], 'cert.pem', { type: 'text/plain' }); - const keyFile = new File(['key'], 'key.pem', { type: 'text/plain' }); - - const result = await uploadCertificate('My Cert', certFile, keyFile); - - expect(client.post).toHaveBeenCalledWith('/certificates', expect.any(FormData), { - headers: { 'Content-Type': 'multipart/form-data' }, - }); - expect(result).toEqual(mockCert); - }); - - it('deleteCertificate calls client.delete', async () => { - vi.mocked(client.delete).mockResolvedValue({ data: {} }); - await deleteCertificate(1); - expect(client.delete).toHaveBeenCalledWith('/certificates/1'); - }); -}); diff --git a/frontend/src/api/__tests__/consoleEnrollment.test.ts b/frontend/src/api/__tests__/consoleEnrollment.test.ts deleted file mode 100644 index e1f890b4..00000000 --- a/frontend/src/api/__tests__/consoleEnrollment.test.ts +++ /dev/null @@ -1,507 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' -import * as consoleEnrollment from '../consoleEnrollment' -import client from '../client' - -vi.mock('../client') - -describe('consoleEnrollment API', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - describe('getConsoleStatus', () => { - it('should fetch enrollment status with pending state', async () => { - const mockStatus = { - status: 'pending', - tenant: 'my-org', - agent_name: 'charon-prod', - key_present: true, - last_attempt_at: '2025-12-15T09:00:00Z', - } - vi.mocked(client.get).mockResolvedValue({ data: mockStatus }) - - const result = await consoleEnrollment.getConsoleStatus() - - expect(client.get).toHaveBeenCalledWith('/admin/crowdsec/console/status') - expect(result).toEqual(mockStatus) - expect(result.status).toBe('pending') - expect(result.key_present).toBe(true) - }) - - it('should fetch enrolled status with heartbeat', async () => { - const mockStatus = { - status: 'enrolled', - tenant: 'my-org', - agent_name: 'charon-prod', - key_present: true, - enrolled_at: '2025-12-14T10:00:00Z', - last_heartbeat_at: '2025-12-15T09:55:00Z', - } - vi.mocked(client.get).mockResolvedValue({ data: mockStatus }) - - const result = await consoleEnrollment.getConsoleStatus() - - expect(result.status).toBe('enrolled') - expect(result.enrolled_at).toBeDefined() - expect(result.last_heartbeat_at).toBeDefined() - }) - - it('should fetch failed status with error message', async () => { - const mockStatus = { - status: 'failed', - tenant: 'my-org', - agent_name: 'charon-prod', - key_present: false, - last_error: 'Invalid enrollment key', - last_attempt_at: '2025-12-15T09:00:00Z', - correlation_id: 'req-abc123', - } - vi.mocked(client.get).mockResolvedValue({ data: mockStatus }) - - const result = await consoleEnrollment.getConsoleStatus() - - expect(result.status).toBe('failed') - expect(result.last_error).toBe('Invalid enrollment key') - expect(result.correlation_id).toBe('req-abc123') - expect(result.key_present).toBe(false) - }) - - it('should fetch status with none state (not enrolled)', async () => { - const mockStatus = { - status: 'none', - key_present: false, - } - vi.mocked(client.get).mockResolvedValue({ data: mockStatus }) - - const result = await consoleEnrollment.getConsoleStatus() - - expect(result.status).toBe('none') - expect(result.key_present).toBe(false) - expect(result.tenant).toBeUndefined() - }) - - it('should NOT return enrollment key in status response', async () => { - const mockStatus = { - status: 'enrolled', - tenant: 'test-org', - agent_name: 'test-agent', - key_present: true, - enrolled_at: '2025-12-14T10:00:00Z', - } - vi.mocked(client.get).mockResolvedValue({ data: mockStatus }) - - const result = await consoleEnrollment.getConsoleStatus() - - // Security test: Ensure key is never exposed - expect(result).not.toHaveProperty('enrollment_key') - expect(result).not.toHaveProperty('encrypted_enroll_key') - expect(result).toHaveProperty('key_present') - }) - - it('should handle API errors', async () => { - const error = new Error('Network error') - vi.mocked(client.get).mockRejectedValue(error) - - await expect(consoleEnrollment.getConsoleStatus()).rejects.toThrow('Network error') - }) - - it('should handle server unavailability', async () => { - const error = { - response: { - status: 503, - data: { error: 'Service temporarily unavailable' }, - }, - } - vi.mocked(client.get).mockRejectedValue(error) - - await expect(consoleEnrollment.getConsoleStatus()).rejects.toEqual(error) - }) - }) - - describe('enrollConsole', () => { - it('should enroll with valid payload', async () => { - const payload = { - enrollment_key: 'cs-enroll-abc123xyz', - tenant: 'my-org', - agent_name: 'charon-prod', - force: false, - } - const mockResponse = { - status: 'enrolled', - tenant: 'my-org', - agent_name: 'charon-prod', - key_present: true, - enrolled_at: '2025-12-15T10:00:00Z', - } - vi.mocked(client.post).mockResolvedValue({ data: mockResponse }) - - const result = await consoleEnrollment.enrollConsole(payload) - - expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/console/enroll', payload) - expect(result).toEqual(mockResponse) - expect(result.status).toBe('enrolled') - expect(result.enrolled_at).toBeDefined() - }) - - it('should enroll with minimal payload (no tenant)', async () => { - const payload = { - enrollment_key: 'cs-enroll-key123', - agent_name: 'charon-test', - } - const mockResponse = { - status: 'enrolled', - agent_name: 'charon-test', - key_present: true, - enrolled_at: '2025-12-15T10:00:00Z', - } - vi.mocked(client.post).mockResolvedValue({ data: mockResponse }) - - const result = await consoleEnrollment.enrollConsole(payload) - - expect(result.status).toBe('enrolled') - expect(result.agent_name).toBe('charon-test') - }) - - it('should force re-enrollment when force=true', async () => { - const payload = { - enrollment_key: 'cs-enroll-new-key', - agent_name: 'charon-updated', - force: true, - } - const mockResponse = { - status: 'enrolled', - agent_name: 'charon-updated', - key_present: true, - enrolled_at: '2025-12-15T10:05:00Z', - } - vi.mocked(client.post).mockResolvedValue({ data: mockResponse }) - - const result = await consoleEnrollment.enrollConsole(payload) - - expect(result.status).toBe('enrolled') - expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/console/enroll', payload) - }) - - it('should handle invalid enrollment key format', async () => { - const payload = { - enrollment_key: 'not-a-valid-key', - agent_name: 'test', - } - const error = { - response: { - status: 400, - data: { error: 'Invalid enrollment key format' }, - }, - } - vi.mocked(client.post).mockRejectedValue(error) - - await expect(consoleEnrollment.enrollConsole(payload)).rejects.toEqual(error) - }) - - it('should handle transient network errors during enrollment', async () => { - const payload = { - enrollment_key: 'cs-enroll-key123', - agent_name: 'test-agent', - } - const error = { - response: { - status: 503, - data: { error: 'CrowdSec Console API temporarily unavailable' }, - }, - } - vi.mocked(client.post).mockRejectedValue(error) - - await expect(consoleEnrollment.enrollConsole(payload)).rejects.toEqual(error) - }) - - it('should handle enrollment key expiration', async () => { - const payload = { - enrollment_key: 'cs-enroll-expired-key', - agent_name: 'test', - } - const mockResponse = { - status: 'failed', - key_present: false, - last_error: 'Enrollment key expired', - correlation_id: 'err-expired-123', - } - vi.mocked(client.post).mockResolvedValue({ data: mockResponse }) - - const result = await consoleEnrollment.enrollConsole(payload) - - expect(result.status).toBe('failed') - expect(result.last_error).toBe('Enrollment key expired') - }) - - it('should sanitize tenant name with special characters', async () => { - const payload = { - enrollment_key: 'valid-key', - tenant: 'My Org (Production)', - agent_name: 'agent1', - } - const mockResponse = { - status: 'enrolled', - tenant: 'My Org (Production)', - agent_name: 'agent1', - key_present: true, - enrolled_at: '2025-12-15T10:00:00Z', - } - vi.mocked(client.post).mockResolvedValue({ data: mockResponse }) - - const result = await consoleEnrollment.enrollConsole(payload) - - expect(result.status).toBe('enrolled') - expect(result.tenant).toBe('My Org (Production)') - }) - - it('should handle SQL injection attempts in agent_name', async () => { - const payload = { - enrollment_key: 'valid-key', - agent_name: "'; DROP TABLE users; --", - } - const error = { - response: { - status: 400, - data: { error: 'Invalid agent name format' }, - }, - } - vi.mocked(client.post).mockRejectedValue(error) - - await expect(consoleEnrollment.enrollConsole(payload)).rejects.toEqual(error) - }) - - it('should handle CrowdSec not running during enrollment', async () => { - const payload = { - enrollment_key: 'valid-key', - agent_name: 'test', - } - const error = { - response: { - status: 500, - data: { error: 'CrowdSec is not running. Start CrowdSec before enrolling.' }, - }, - } - vi.mocked(client.post).mockRejectedValue(error) - - await expect(consoleEnrollment.enrollConsole(payload)).rejects.toEqual(error) - }) - - it('should return pending status when enrollment is queued', async () => { - const payload = { - enrollment_key: 'valid-key', - agent_name: 'test', - } - const mockResponse = { - status: 'pending', - agent_name: 'test', - key_present: true, - last_attempt_at: '2025-12-15T10:00:00Z', - } - vi.mocked(client.post).mockResolvedValue({ data: mockResponse }) - - const result = await consoleEnrollment.enrollConsole(payload) - - expect(result.status).toBe('pending') - expect(result.last_attempt_at).toBeDefined() - }) - }) - - describe('default export', () => { - it('should export all functions', () => { - expect(consoleEnrollment.default).toHaveProperty('getConsoleStatus') - expect(consoleEnrollment.default).toHaveProperty('enrollConsole') - }) - }) - - describe('integration scenarios', () => { - it('should handle full enrollment workflow: status → enroll → verify', async () => { - // 1. Check initial status (not enrolled) - const mockStatusNone = { - status: 'none', - key_present: false, - } - vi.mocked(client.get).mockResolvedValueOnce({ data: mockStatusNone }) - - const statusBefore = await consoleEnrollment.getConsoleStatus() - expect(statusBefore.status).toBe('none') - - // 2. Enroll - const enrollPayload = { - enrollment_key: 'cs-enroll-valid-key', - tenant: 'test-org', - agent_name: 'charon-test', - } - const mockEnrollResponse = { - status: 'enrolled', - tenant: 'test-org', - agent_name: 'charon-test', - key_present: true, - enrolled_at: '2025-12-15T10:00:00Z', - } - vi.mocked(client.post).mockResolvedValueOnce({ data: mockEnrollResponse }) - - const enrollResult = await consoleEnrollment.enrollConsole(enrollPayload) - expect(enrollResult.status).toBe('enrolled') - - // 3. Verify status updated - const mockStatusEnrolled = { - status: 'enrolled', - tenant: 'test-org', - agent_name: 'charon-test', - key_present: true, - enrolled_at: '2025-12-15T10:00:00Z', - last_heartbeat_at: '2025-12-15T10:01:00Z', - } - vi.mocked(client.get).mockResolvedValueOnce({ data: mockStatusEnrolled }) - - const statusAfter = await consoleEnrollment.getConsoleStatus() - expect(statusAfter.status).toBe('enrolled') - expect(statusAfter.tenant).toBe('test-org') - }) - - it('should handle enrollment failure and retry', async () => { - // 1. First enrollment attempt fails - const payload = { - enrollment_key: 'cs-enroll-key', - agent_name: 'test', - } - const networkError = new Error('Network timeout') - vi.mocked(client.post).mockRejectedValueOnce(networkError) - - await expect(consoleEnrollment.enrollConsole(payload)).rejects.toThrow('Network timeout') - - // 2. Retry succeeds - const mockResponse = { - status: 'enrolled', - agent_name: 'test', - key_present: true, - enrolled_at: '2025-12-15T10:05:00Z', - } - vi.mocked(client.post).mockResolvedValueOnce({ data: mockResponse }) - - const retryResult = await consoleEnrollment.enrollConsole(payload) - expect(retryResult.status).toBe('enrolled') - }) - - it('should handle status transitions: none → pending → enrolled', async () => { - // 1. Initial: none - const mockNone = { status: 'none', key_present: false } - vi.mocked(client.get).mockResolvedValueOnce({ data: mockNone }) - const status1 = await consoleEnrollment.getConsoleStatus() - expect(status1.status).toBe('none') - - // 2. Enroll (returns pending) - const payload = { enrollment_key: 'key', agent_name: 'agent' } - const mockPending = { - status: 'pending', - agent_name: 'agent', - key_present: true, - last_attempt_at: '2025-12-15T10:00:00Z', - } - vi.mocked(client.post).mockResolvedValueOnce({ data: mockPending }) - const enrollResult = await consoleEnrollment.enrollConsole(payload) - expect(enrollResult.status).toBe('pending') - - // 3. Check status again (now enrolled) - const mockEnrolled = { - status: 'enrolled', - agent_name: 'agent', - key_present: true, - enrolled_at: '2025-12-15T10:00:30Z', - } - vi.mocked(client.get).mockResolvedValueOnce({ data: mockEnrolled }) - const status2 = await consoleEnrollment.getConsoleStatus() - expect(status2.status).toBe('enrolled') - }) - - it('should handle force re-enrollment over existing enrollment', async () => { - // 1. Check current enrollment - const mockCurrent = { - status: 'enrolled', - tenant: 'old-org', - agent_name: 'old-agent', - key_present: true, - enrolled_at: '2025-12-14T10:00:00Z', - } - vi.mocked(client.get).mockResolvedValueOnce({ data: mockCurrent }) - const currentStatus = await consoleEnrollment.getConsoleStatus() - expect(currentStatus.tenant).toBe('old-org') - - // 2. Force re-enrollment - const forcePayload = { - enrollment_key: 'new-key', - tenant: 'new-org', - agent_name: 'new-agent', - force: true, - } - const mockForced = { - status: 'enrolled', - tenant: 'new-org', - agent_name: 'new-agent', - key_present: true, - enrolled_at: '2025-12-15T10:00:00Z', - } - vi.mocked(client.post).mockResolvedValueOnce({ data: mockForced }) - const forceResult = await consoleEnrollment.enrollConsole(forcePayload) - expect(forceResult.tenant).toBe('new-org') - }) - }) - - describe('security tests', () => { - it('should never log or expose enrollment key', async () => { - const payload = { - enrollment_key: 'cs-enroll-secret-key-should-never-log', - agent_name: 'test', - } - const mockResponse = { - status: 'enrolled', - agent_name: 'test', - key_present: true, - } - vi.mocked(client.post).mockResolvedValue({ data: mockResponse }) - - const result = await consoleEnrollment.enrollConsole(payload) - - // Ensure response never contains the key - expect(result).not.toHaveProperty('enrollment_key') - expect(JSON.stringify(result)).not.toContain('cs-enroll-secret-key') - }) - - it('should sanitize error messages to avoid key leakage', async () => { - const payload = { - enrollment_key: 'cs-enroll-sensitive-key', - agent_name: 'test', - } - const error = { - response: { - status: 400, - data: { error: 'Enrollment failed: invalid key format' }, - }, - } - vi.mocked(client.post).mockRejectedValue(error) - - try { - await consoleEnrollment.enrollConsole(payload) - } catch (e: unknown) { - // Error message should NOT contain the key - const error = e as { response?: { data?: { error?: string } } } - expect(error.response?.data?.error).not.toContain('cs-enroll-sensitive-key') - } - }) - - it('should handle correlation_id for debugging without exposing keys', async () => { - const mockStatus = { - status: 'failed', - key_present: false, - last_error: 'Authentication failed', - correlation_id: 'debug-correlation-abc123', - } - vi.mocked(client.get).mockResolvedValue({ data: mockStatus }) - - const result = await consoleEnrollment.getConsoleStatus() - - expect(result.correlation_id).toBe('debug-correlation-abc123') - expect(result).not.toHaveProperty('enrollment_key') - }) - }) -}) diff --git a/frontend/src/api/__tests__/crowdsec.test.ts b/frontend/src/api/__tests__/crowdsec.test.ts deleted file mode 100644 index 34460efa..00000000 --- a/frontend/src/api/__tests__/crowdsec.test.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' -import * as crowdsec from '../crowdsec' -import client from '../client' - -vi.mock('../client') - -describe('crowdsec API', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - describe('startCrowdsec', () => { - it('should call POST /admin/crowdsec/start', async () => { - const mockData = { success: true } - vi.mocked(client.post).mockResolvedValue({ data: mockData }) - - const result = await crowdsec.startCrowdsec() - - expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/start') - expect(result).toEqual(mockData) - }) - }) - - describe('stopCrowdsec', () => { - it('should call POST /admin/crowdsec/stop', async () => { - const mockData = { success: true } - vi.mocked(client.post).mockResolvedValue({ data: mockData }) - - const result = await crowdsec.stopCrowdsec() - - expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/stop') - expect(result).toEqual(mockData) - }) - }) - - describe('statusCrowdsec', () => { - it('should call GET /admin/crowdsec/status', async () => { - const mockData = { running: true, pid: 1234 } - vi.mocked(client.get).mockResolvedValue({ data: mockData }) - - const result = await crowdsec.statusCrowdsec() - - expect(client.get).toHaveBeenCalledWith('/admin/crowdsec/status') - expect(result).toEqual(mockData) - }) - }) - - describe('importCrowdsecConfig', () => { - it('should call POST /admin/crowdsec/import with FormData', async () => { - const mockFile = new File(['content'], 'config.tar.gz', { type: 'application/gzip' }) - const mockData = { success: true } - vi.mocked(client.post).mockResolvedValue({ data: mockData }) - - const result = await crowdsec.importCrowdsecConfig(mockFile) - - expect(client.post).toHaveBeenCalledWith( - '/admin/crowdsec/import', - expect.any(FormData), - { headers: { 'Content-Type': 'multipart/form-data' } } - ) - expect(result).toEqual(mockData) - }) - }) - - describe('exportCrowdsecConfig', () => { - it('should call GET /admin/crowdsec/export with blob responseType', async () => { - const mockBlob = new Blob(['data'], { type: 'application/gzip' }) - vi.mocked(client.get).mockResolvedValue({ data: mockBlob }) - - const result = await crowdsec.exportCrowdsecConfig() - - expect(client.get).toHaveBeenCalledWith('/admin/crowdsec/export', { responseType: 'blob' }) - expect(result).toEqual(mockBlob) - }) - }) - - describe('listCrowdsecFiles', () => { - it('should call GET /admin/crowdsec/files', async () => { - const mockData = { files: ['file1.yaml', 'file2.yaml'] } - vi.mocked(client.get).mockResolvedValue({ data: mockData }) - - const result = await crowdsec.listCrowdsecFiles() - - expect(client.get).toHaveBeenCalledWith('/admin/crowdsec/files') - expect(result).toEqual(mockData) - }) - }) - - describe('readCrowdsecFile', () => { - it('should call GET /admin/crowdsec/file with encoded path', async () => { - const mockData = { content: 'file content' } - const path = '/etc/crowdsec/file.yaml' - vi.mocked(client.get).mockResolvedValue({ data: mockData }) - - const result = await crowdsec.readCrowdsecFile(path) - - expect(client.get).toHaveBeenCalledWith( - `/admin/crowdsec/file?path=${encodeURIComponent(path)}` - ) - expect(result).toEqual(mockData) - }) - }) - - describe('writeCrowdsecFile', () => { - it('should call POST /admin/crowdsec/file with path and content', async () => { - const mockData = { success: true } - const path = '/etc/crowdsec/file.yaml' - const content = 'new content' - vi.mocked(client.post).mockResolvedValue({ data: mockData }) - - const result = await crowdsec.writeCrowdsecFile(path, content) - - expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/file', { path, content }) - expect(result).toEqual(mockData) - }) - }) - - describe('default export', () => { - it('should export all functions', () => { - expect(crowdsec.default).toHaveProperty('startCrowdsec') - expect(crowdsec.default).toHaveProperty('stopCrowdsec') - expect(crowdsec.default).toHaveProperty('statusCrowdsec') - expect(crowdsec.default).toHaveProperty('importCrowdsecConfig') - expect(crowdsec.default).toHaveProperty('exportCrowdsecConfig') - expect(crowdsec.default).toHaveProperty('listCrowdsecFiles') - expect(crowdsec.default).toHaveProperty('readCrowdsecFile') - expect(crowdsec.default).toHaveProperty('writeCrowdsecFile') - }) - }) -}) diff --git a/frontend/src/api/__tests__/docker.test.ts b/frontend/src/api/__tests__/docker.test.ts deleted file mode 100644 index 0a435e6c..00000000 --- a/frontend/src/api/__tests__/docker.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { vi, describe, it, expect, beforeEach } from 'vitest'; -import { dockerApi } from '../docker'; -import client from '../client'; - -vi.mock('../client', () => ({ - default: { - get: vi.fn(), - }, -})); - -describe('dockerApi', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('listContainers', () => { - const mockContainers = [ - { - id: 'abc123', - names: ['/container1'], - image: 'nginx:latest', - state: 'running', - status: 'Up 2 hours', - network: 'bridge', - ip: '172.17.0.2', - ports: [{ private_port: 80, public_port: 8080, type: 'tcp' }], - }, - { - id: 'def456', - names: ['/container2'], - image: 'redis:alpine', - state: 'running', - status: 'Up 1 hour', - network: 'bridge', - ip: '172.17.0.3', - ports: [], - }, - ]; - - it('fetches containers without parameters', async () => { - vi.mocked(client.get).mockResolvedValue({ data: mockContainers }); - - const result = await dockerApi.listContainers(); - - expect(client.get).toHaveBeenCalledWith('/docker/containers', { params: {} }); - expect(result).toEqual(mockContainers); - }); - - it('fetches containers with host parameter', async () => { - vi.mocked(client.get).mockResolvedValue({ data: mockContainers }); - - const result = await dockerApi.listContainers('192.168.1.100'); - - expect(client.get).toHaveBeenCalledWith('/docker/containers', { - params: { host: '192.168.1.100' }, - }); - expect(result).toEqual(mockContainers); - }); - - it('fetches containers with serverId parameter', async () => { - vi.mocked(client.get).mockResolvedValue({ data: mockContainers }); - - const result = await dockerApi.listContainers(undefined, 'server-uuid-123'); - - expect(client.get).toHaveBeenCalledWith('/docker/containers', { - params: { server_id: 'server-uuid-123' }, - }); - expect(result).toEqual(mockContainers); - }); - - it('fetches containers with both host and serverId parameters', async () => { - vi.mocked(client.get).mockResolvedValue({ data: mockContainers }); - - const result = await dockerApi.listContainers('192.168.1.100', 'server-uuid-123'); - - expect(client.get).toHaveBeenCalledWith('/docker/containers', { - params: { host: '192.168.1.100', server_id: 'server-uuid-123' }, - }); - expect(result).toEqual(mockContainers); - }); - - it('returns empty array when no containers', async () => { - vi.mocked(client.get).mockResolvedValue({ data: [] }); - - const result = await dockerApi.listContainers(); - - expect(result).toEqual([]); - }); - - it('handles API error', async () => { - vi.mocked(client.get).mockRejectedValue(new Error('Network error')); - - await expect(dockerApi.listContainers()).rejects.toThrow('Network error'); - }); - }); -}); diff --git a/frontend/src/api/__tests__/domains.test.ts b/frontend/src/api/__tests__/domains.test.ts deleted file mode 100644 index 3181876e..00000000 --- a/frontend/src/api/__tests__/domains.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import client from '../client'; -import { getDomains, createDomain, deleteDomain, Domain } from '../domains'; - -vi.mock('../client', () => ({ - default: { - get: vi.fn(), - post: vi.fn(), - delete: vi.fn(), - }, -})); - -describe('domains API', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - const mockDomain: Domain = { - id: 1, - uuid: '123', - name: 'example.com', - created_at: '2023-01-01', - }; - - it('getDomains calls client.get', async () => { - vi.mocked(client.get).mockResolvedValue({ data: [mockDomain] }); - const result = await getDomains(); - expect(client.get).toHaveBeenCalledWith('/domains'); - expect(result).toEqual([mockDomain]); - }); - - it('createDomain calls client.post', async () => { - vi.mocked(client.post).mockResolvedValue({ data: mockDomain }); - const result = await createDomain('example.com'); - expect(client.post).toHaveBeenCalledWith('/domains', { name: 'example.com' }); - expect(result).toEqual(mockDomain); - }); - - it('deleteDomain calls client.delete', async () => { - vi.mocked(client.delete).mockResolvedValue({ data: {} }); - await deleteDomain('123'); - expect(client.delete).toHaveBeenCalledWith('/domains/123'); - }); -}); diff --git a/frontend/src/api/__tests__/logs-websocket.test.ts b/frontend/src/api/__tests__/logs-websocket.test.ts deleted file mode 100644 index 912db2df..00000000 --- a/frontend/src/api/__tests__/logs-websocket.test.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { connectLiveLogs } from '../logs'; - -// Mock WebSocket -class MockWebSocket { - url: string; - onmessage: ((event: MessageEvent) => void) | null = null; - onerror: ((error: Event) => void) | null = null; - onclose: ((event: CloseEvent) => void) | null = null; - readyState: number = WebSocket.CONNECTING; - - static CONNECTING = 0; - static OPEN = 1; - static CLOSING = 2; - static CLOSED = 3; - - constructor(url: string) { - this.url = url; - // Simulate connection opening - setTimeout(() => { - this.readyState = WebSocket.OPEN; - }, 0); - } - - close() { - this.readyState = WebSocket.CLOSING; - setTimeout(() => { - this.readyState = WebSocket.CLOSED; - const closeEvent = { code: 1000, reason: '', wasClean: true } as CloseEvent; - if (this.onclose) { - this.onclose(closeEvent); - } - }, 0); - } - - simulateMessage(data: string) { - if (this.onmessage) { - const event = new MessageEvent('message', { data }); - this.onmessage(event); - } - } - - simulateError() { - if (this.onerror) { - const event = new Event('error'); - this.onerror(event); - } - } -} - -describe('logs API - connectLiveLogs', () => { - let mockWebSocket: MockWebSocket; - - beforeEach(() => { - // Mock global WebSocket - mockWebSocket = new MockWebSocket(''); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (globalThis as any).WebSocket = class MockedWebSocket extends MockWebSocket { - constructor(url: string) { - super(url); - // eslint-disable-next-line @typescript-eslint/no-this-alias - mockWebSocket = this; - } - } as unknown as typeof WebSocket; - - // Mock window.location - Object.defineProperty(window, 'location', { - value: { - protocol: 'http:', - host: 'localhost:8080', - }, - writable: true, - }); - }); - - it('creates WebSocket connection with correct URL', () => { - connectLiveLogs({}, vi.fn()); - - expect(mockWebSocket.url).toBe('ws://localhost:8080/api/v1/logs/live?'); - }); - - it('uses wss protocol when page is https', () => { - Object.defineProperty(window, 'location', { - value: { - protocol: 'https:', - host: 'example.com', - }, - writable: true, - }); - - connectLiveLogs({}, vi.fn()); - - expect(mockWebSocket.url).toBe('wss://example.com/api/v1/logs/live?'); - }); - - it('includes filters in query parameters', () => { - connectLiveLogs({ level: 'error', source: 'waf' }, vi.fn()); - - expect(mockWebSocket.url).toContain('level=error'); - expect(mockWebSocket.url).toContain('source=waf'); - }); - - it('calls onMessage callback when message is received', () => { - const mockOnMessage = vi.fn(); - connectLiveLogs({}, mockOnMessage); - - const logData = { - level: 'info', - timestamp: '2025-12-09T10:30:00Z', - message: 'Test message', - }; - - mockWebSocket.simulateMessage(JSON.stringify(logData)); - - expect(mockOnMessage).toHaveBeenCalledWith(logData); - }); - - it('handles JSON parse errors gracefully', () => { - const mockOnMessage = vi.fn(); - const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - - connectLiveLogs({}, mockOnMessage); - - mockWebSocket.simulateMessage('invalid json'); - - expect(mockOnMessage).not.toHaveBeenCalled(); - expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to parse log message:', expect.any(Error)); - - consoleErrorSpy.mockRestore(); - }); - - // These tests are skipped because the WebSocket mock has timing issues with event handlers - // The functionality is covered by E2E tests - it.skip('calls onError callback when error occurs', async () => { - const mockOnError = vi.fn(); - const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - - connectLiveLogs({}, vi.fn(), mockOnError); - - // Wait for handlers to be set up - await new Promise(resolve => setTimeout(resolve, 10)); - - mockWebSocket.simulateError(); - - expect(mockOnError).toHaveBeenCalled(); - expect(consoleErrorSpy).toHaveBeenCalledWith('WebSocket error:', expect.any(Event)); - - consoleErrorSpy.mockRestore(); - }); - - it.skip('calls onClose callback when connection closes', async () => { - const mockOnClose = vi.fn(); - const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - - connectLiveLogs({}, vi.fn(), undefined, mockOnClose); - - // Wait for handlers to be set up - await new Promise(resolve => setTimeout(resolve, 10)); - - mockWebSocket.close(); - - // Wait for the close event to be processed - await new Promise(resolve => setTimeout(resolve, 20)); - - expect(mockOnClose).toHaveBeenCalled(); - consoleLogSpy.mockRestore(); - }); - - it('returns a close function that closes the WebSocket', async () => { - const closeConnection = connectLiveLogs({}, vi.fn()); - - // Wait for connection to open - await new Promise(resolve => setTimeout(resolve, 10)); - - expect(mockWebSocket.readyState).toBe(WebSocket.OPEN); - - closeConnection(); - - expect(mockWebSocket.readyState).toBeGreaterThanOrEqual(WebSocket.CLOSING); - }); - - it('does not throw when closing already closed connection', () => { - const closeConnection = connectLiveLogs({}, vi.fn()); - - mockWebSocket.readyState = WebSocket.CLOSED; - - expect(() => closeConnection()).not.toThrow(); - }); - - it('handles missing optional callbacks', () => { - // Should not throw with only required onMessage callback - expect(() => connectLiveLogs({}, vi.fn())).not.toThrow(); - - const mockOnMessage = vi.fn(); - connectLiveLogs({}, mockOnMessage); - - // Simulate various events - mockWebSocket.simulateMessage(JSON.stringify({ level: 'info', timestamp: '2025-12-09T10:30:00Z', message: 'test' })); - mockWebSocket.simulateError(); - - expect(mockOnMessage).toHaveBeenCalled(); - }); - - it('processes multiple messages in sequence', () => { - const mockOnMessage = vi.fn(); - connectLiveLogs({}, mockOnMessage); - - const log1 = { level: 'info', timestamp: '2025-12-09T10:30:00Z', message: 'Message 1' }; - const log2 = { level: 'error', timestamp: '2025-12-09T10:30:01Z', message: 'Message 2' }; - - mockWebSocket.simulateMessage(JSON.stringify(log1)); - mockWebSocket.simulateMessage(JSON.stringify(log2)); - - expect(mockOnMessage).toHaveBeenCalledTimes(2); - expect(mockOnMessage).toHaveBeenNthCalledWith(1, log1); - expect(mockOnMessage).toHaveBeenNthCalledWith(2, log2); - }); -}); diff --git a/frontend/src/api/__tests__/logs.http.test.ts b/frontend/src/api/__tests__/logs.http.test.ts deleted file mode 100644 index b9e0067f..00000000 --- a/frontend/src/api/__tests__/logs.http.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' -import client from '../client' -import { downloadLog, getLogContent, getLogs } from '../logs' - -vi.mock('../client', () => ({ - default: { - get: vi.fn(), - }, -})) - -describe('logs api http helpers', () => { - beforeEach(() => { - vi.clearAllMocks() - Object.defineProperty(window, 'location', { - value: { href: 'http://localhost' }, - writable: true, - }) - }) - - it('fetches log list and content with filters', async () => { - vi.mocked(client.get).mockResolvedValueOnce({ data: [{ name: 'access.log', size: 10, mod_time: 'now' }] }) - const logs = await getLogs() - expect(logs[0].name).toBe('access.log') - expect(client.get).toHaveBeenCalledWith('/logs') - - vi.mocked(client.get).mockResolvedValueOnce({ data: { filename: 'access.log', logs: [], total: 0, limit: 100, offset: 0 } }) - const resp = await getLogContent('access.log', { - search: 'bot', - host: 'example.com', - status: '500', - level: 'error', - limit: 50, - offset: 5, - sort: 'asc', - }) - expect(resp.filename).toBe('access.log') - expect(client.get).toHaveBeenCalledWith('/logs/access.log?search=bot&host=example.com&status=500&level=error&limit=50&offset=5&sort=asc') - }) - - it('downloads log via window location', () => { - downloadLog('access.log') - expect(window.location.href).toBe('/api/v1/logs/access.log/download') - }) -}) diff --git a/frontend/src/api/__tests__/notifications.test.ts b/frontend/src/api/__tests__/notifications.test.ts deleted file mode 100644 index 0c641b27..00000000 --- a/frontend/src/api/__tests__/notifications.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' -import client from '../client' -import { - getProviders, - createProvider, - updateProvider, - deleteProvider, - testProvider, - getTemplates, - previewProvider, - getExternalTemplates, - createExternalTemplate, - updateExternalTemplate, - deleteExternalTemplate, - previewExternalTemplate, - getSecurityNotificationSettings, - updateSecurityNotificationSettings, -} from '../notifications' - -vi.mock('../client', () => ({ - default: { - get: vi.fn(), - post: vi.fn(), - put: vi.fn(), - delete: vi.fn(), - }, -})) - -describe('notifications api', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it('crud for providers uses correct endpoints', async () => { - vi.mocked(client.get).mockResolvedValue({ data: [{ id: '1', name: 'webhook', type: 'webhook', url: 'http://', enabled: true } as never] }) - vi.mocked(client.post).mockResolvedValue({ data: { id: '2' } }) - vi.mocked(client.put).mockResolvedValue({ data: { id: '2', name: 'updated' } }) - - const providers = await getProviders() - expect(providers[0].id).toBe('1') - expect(client.get).toHaveBeenCalledWith('/notifications/providers') - - await createProvider({ name: 'x' }) - expect(client.post).toHaveBeenCalledWith('/notifications/providers', { name: 'x' }) - - await updateProvider('2', { name: 'updated' }) - expect(client.put).toHaveBeenCalledWith('/notifications/providers/2', { name: 'updated' }) - - await deleteProvider('2') - expect(client.delete).toHaveBeenCalledWith('/notifications/providers/2') - - await testProvider({ id: '2', name: 'test' }) - expect(client.post).toHaveBeenCalledWith('/notifications/providers/test', { id: '2', name: 'test' }) - }) - - it('templates and previews use merged payloads', async () => { - vi.mocked(client.get).mockResolvedValueOnce({ data: [{ id: 't1', name: 'default' }] }) - const templates = await getTemplates() - expect(templates[0].name).toBe('default') - expect(client.get).toHaveBeenCalledWith('/notifications/templates') - - vi.mocked(client.post).mockResolvedValueOnce({ data: { preview: 'ok' } }) - const preview = await previewProvider({ name: 'provider' }, { user: 'alice' }) - expect(preview).toEqual({ preview: 'ok' }) - expect(client.post).toHaveBeenCalledWith('/notifications/providers/preview', { name: 'provider', data: { user: 'alice' } }) - }) - - it('external template endpoints shape payloads', async () => { - vi.mocked(client.get).mockResolvedValueOnce({ data: [{ id: 'ext', name: 'External' }] }) - const external = await getExternalTemplates() - expect(external[0].id).toBe('ext') - expect(client.get).toHaveBeenCalledWith('/notifications/external-templates') - - vi.mocked(client.post).mockResolvedValueOnce({ data: { id: 'ext2' } }) - await createExternalTemplate({ name: 'n' }) - expect(client.post).toHaveBeenCalledWith('/notifications/external-templates', { name: 'n' }) - - vi.mocked(client.put).mockResolvedValueOnce({ data: { id: 'ext', name: 'updated' } }) - await updateExternalTemplate('ext', { name: 'updated' }) - expect(client.put).toHaveBeenCalledWith('/notifications/external-templates/ext', { name: 'updated' }) - - await deleteExternalTemplate('ext') - expect(client.delete).toHaveBeenCalledWith('/notifications/external-templates/ext') - - vi.mocked(client.post).mockResolvedValueOnce({ data: { rendered: true } }) - const result = await previewExternalTemplate('ext', 'tpl', { id: 1 }) - expect(result).toEqual({ rendered: true }) - expect(client.post).toHaveBeenCalledWith('/notifications/external-templates/preview', { template_id: 'ext', template: 'tpl', data: { id: 1 } }) - }) - - it('reads and updates security notification settings', async () => { - vi.mocked(client.get).mockResolvedValueOnce({ data: { enabled: true, min_log_level: 'info', notify_waf_blocks: true } }) - const settings = await getSecurityNotificationSettings() - expect(settings.enabled).toBe(true) - expect(client.get).toHaveBeenCalledWith('/notifications/settings/security') - - vi.mocked(client.put).mockResolvedValueOnce({ data: { enabled: false } }) - const updated = await updateSecurityNotificationSettings({ enabled: false }) - expect(updated.enabled).toBe(false) - expect(client.put).toHaveBeenCalledWith('/notifications/settings/security', { enabled: false }) - }) -}) diff --git a/frontend/src/api/__tests__/presets.test.ts b/frontend/src/api/__tests__/presets.test.ts deleted file mode 100644 index 064ed91d..00000000 --- a/frontend/src/api/__tests__/presets.test.ts +++ /dev/null @@ -1,465 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' -import * as presets from '../presets' -import client from '../client' - -vi.mock('../client') - -describe('presets API', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - describe('listCrowdsecPresets', () => { - it('should fetch presets list with cached flags', async () => { - const mockPresets = { - presets: [ - { - slug: 'bot-mitigation-essentials', - title: 'Bot Mitigation Essentials', - summary: 'Core HTTP parsers and scenarios', - source: 'hub', - tags: ['bots', 'web'], - requires_hub: true, - available: true, - cached: true, - cache_key: 'hub-bot-abc123', - etag: '"w/12345"', - retrieved_at: '2025-12-15T10:00:00Z', - }, - { - slug: 'honeypot-friendly-defaults', - title: 'Honeypot Friendly Defaults', - summary: 'Lightweight defaults for honeypots', - source: 'builtin', - tags: ['low-noise'], - requires_hub: false, - available: true, - cached: false, - }, - ], - } - vi.mocked(client.get).mockResolvedValue({ data: mockPresets }) - - const result = await presets.listCrowdsecPresets() - - expect(client.get).toHaveBeenCalledWith('/admin/crowdsec/presets') - expect(result).toEqual(mockPresets) - expect(result.presets).toHaveLength(2) - expect(result.presets[0].cached).toBe(true) - expect(result.presets[0].cache_key).toBe('hub-bot-abc123') - expect(result.presets[1].cached).toBe(false) - }) - - it('should handle empty presets list', async () => { - const mockData = { presets: [] } - vi.mocked(client.get).mockResolvedValue({ data: mockData }) - - const result = await presets.listCrowdsecPresets() - - expect(result.presets).toHaveLength(0) - }) - - it('should handle API errors', async () => { - const error = new Error('Network error') - vi.mocked(client.get).mockRejectedValue(error) - - await expect(presets.listCrowdsecPresets()).rejects.toThrow('Network error') - }) - - it('should handle hub API unavailability', async () => { - const error = { - response: { - status: 503, - data: { error: 'CrowdSec Hub API unavailable' }, - }, - } - vi.mocked(client.get).mockRejectedValue(error) - - await expect(presets.listCrowdsecPresets()).rejects.toEqual(error) - }) - }) - - describe('getCrowdsecPresets', () => { - it('should be an alias for listCrowdsecPresets', async () => { - const mockData = { presets: [] } - vi.mocked(client.get).mockResolvedValue({ data: mockData }) - - const result = await presets.getCrowdsecPresets() - - expect(client.get).toHaveBeenCalledWith('/admin/crowdsec/presets') - expect(result).toEqual(mockData) - }) - }) - - describe('pullCrowdsecPreset', () => { - it('should pull preset and return preview with cache_key', async () => { - const mockResponse = { - status: 'success', - slug: 'bot-mitigation-essentials', - preview: '# Bot Mitigation Config\nconfigs:\n collections:\n - crowdsecurity/base-http-scenarios', - cache_key: 'hub-bot-xyz789', - etag: '"abc123"', - retrieved_at: '2025-12-15T10:00:00Z', - source: 'hub', - } - vi.mocked(client.post).mockResolvedValue({ data: mockResponse }) - - const result = await presets.pullCrowdsecPreset('bot-mitigation-essentials') - - expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/presets/pull', { - slug: 'bot-mitigation-essentials', - }) - expect(result).toEqual(mockResponse) - expect(result.status).toBe('success') - expect(result.cache_key).toBeDefined() - expect(result.preview).toContain('configs:') - }) - - it('should handle invalid preset slug', async () => { - const mockResponse = { - status: 'error', - slug: 'non-existent-preset', - preview: '', - cache_key: '', - } - vi.mocked(client.post).mockResolvedValue({ data: mockResponse }) - - const result = await presets.pullCrowdsecPreset('non-existent-preset') - - expect(result.status).toBe('error') - }) - - it('should handle hub API timeout during pull', async () => { - const error = { - response: { - status: 504, - data: { error: 'Gateway timeout while fetching from CrowdSec Hub' }, - }, - } - vi.mocked(client.post).mockRejectedValue(error) - - await expect(presets.pullCrowdsecPreset('bot-mitigation-essentials')).rejects.toEqual(error) - }) - - it('should handle ETAG validation scenarios', async () => { - const mockResponse = { - status: 'success', - slug: 'bot-mitigation-essentials', - preview: '# Cached content', - cache_key: 'hub-bot-cached123', - etag: '"not-modified"', - retrieved_at: '2025-12-14T09:00:00Z', - source: 'cache', - } - vi.mocked(client.post).mockResolvedValue({ data: mockResponse }) - - const result = await presets.pullCrowdsecPreset('bot-mitigation-essentials') - - expect(result.source).toBe('cache') - expect(result.etag).toBe('"not-modified"') - }) - - it('should handle CrowdSec not running during pull', async () => { - const error = { - response: { - status: 500, - data: { error: 'CrowdSec LAPI not available' }, - }, - } - vi.mocked(client.post).mockRejectedValue(error) - - await expect(presets.pullCrowdsecPreset('bot-mitigation-essentials')).rejects.toEqual(error) - }) - - it('should encode special characters in preset slug', async () => { - const mockResponse = { - status: 'success', - slug: 'custom/preset-with-slash', - preview: '# Custom', - cache_key: 'custom-key', - } - vi.mocked(client.post).mockResolvedValue({ data: mockResponse }) - - await presets.pullCrowdsecPreset('custom/preset-with-slash') - - expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/presets/pull', { - slug: 'custom/preset-with-slash', - }) - }) - }) - - describe('applyCrowdsecPreset', () => { - it('should apply preset with cache_key when available', async () => { - const payload = { slug: 'bot-mitigation-essentials', cache_key: 'hub-bot-xyz789' } - const mockResponse = { - status: 'success', - backup: '/data/charon/data/backups/preset-backup-20251215-100000.tar.gz', - reload_hint: true, - used_cscli: true, - cache_key: 'hub-bot-xyz789', - slug: 'bot-mitigation-essentials', - } - vi.mocked(client.post).mockResolvedValue({ data: mockResponse }) - - const result = await presets.applyCrowdsecPreset(payload) - - expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/presets/apply', payload) - expect(result).toEqual(mockResponse) - expect(result.status).toBe('success') - expect(result.backup).toBeDefined() - expect(result.reload_hint).toBe(true) - }) - - it('should apply preset without cache_key (fallback mode)', async () => { - const payload = { slug: 'honeypot-friendly-defaults' } - const mockResponse = { - status: 'success', - backup: '/data/charon/data/backups/preset-backup-20251215-100100.tar.gz', - reload_hint: true, - used_cscli: true, - slug: 'honeypot-friendly-defaults', - } - vi.mocked(client.post).mockResolvedValue({ data: mockResponse }) - - const result = await presets.applyCrowdsecPreset(payload) - - expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/presets/apply', payload) - expect(result.status).toBe('success') - expect(result.used_cscli).toBe(true) - }) - - it('should handle stale cache_key gracefully', async () => { - const stalePayload = { slug: 'bot-mitigation-essentials', cache_key: 'old_key_123' } - const error = { - response: { - status: 400, - data: { error: 'Cache key mismatch or expired. Please pull the preset again.' }, - }, - } - vi.mocked(client.post).mockRejectedValue(error) - - await expect(presets.applyCrowdsecPreset(stalePayload)).rejects.toEqual(error) - }) - - it('should error when applying preset with CrowdSec stopped', async () => { - const payload = { slug: 'bot-mitigation-essentials', cache_key: 'valid-key' } - const error = { - response: { - status: 500, - data: { error: 'CrowdSec is not running. Start CrowdSec before applying presets.' }, - }, - } - vi.mocked(client.post).mockRejectedValue(error) - - await expect(presets.applyCrowdsecPreset(payload)).rejects.toEqual(error) - }) - - it('should handle backup creation failure', async () => { - const payload = { slug: 'bot-mitigation-essentials', cache_key: 'valid-key' } - const error = { - response: { - status: 500, - data: { error: 'Failed to create backup before applying preset' }, - }, - } - vi.mocked(client.post).mockRejectedValue(error) - - await expect(presets.applyCrowdsecPreset(payload)).rejects.toEqual(error) - }) - - it('should handle cscli errors during application', async () => { - const payload = { slug: 'invalid-preset' } - const error = { - response: { - status: 500, - data: { error: 'cscli hub update failed: exit status 1' }, - }, - } - vi.mocked(client.post).mockRejectedValue(error) - - await expect(presets.applyCrowdsecPreset(payload)).rejects.toEqual(error) - }) - - it('should handle payload with force flag', async () => { - const payload = { slug: 'bot-mitigation-essentials', cache_key: 'key123' } - const mockResponse = { - status: 'success', - backup: '/data/backups/preset-forced.tar.gz', - reload_hint: true, - } - vi.mocked(client.post).mockResolvedValue({ data: mockResponse }) - - const result = await presets.applyCrowdsecPreset(payload) - - expect(result.status).toBe('success') - }) - }) - - describe('getCrowdsecPresetCache', () => { - it('should fetch cached preset preview', async () => { - const mockCache = { - preview: '# Cached Bot Mitigation Config\nconfigs:\n collections:\n - crowdsecurity/base-http-scenarios', - cache_key: 'hub-bot-xyz789', - etag: '"abc123"', - } - vi.mocked(client.get).mockResolvedValue({ data: mockCache }) - - const result = await presets.getCrowdsecPresetCache('bot-mitigation-essentials') - - expect(client.get).toHaveBeenCalledWith( - '/admin/crowdsec/presets/cache/bot-mitigation-essentials' - ) - expect(result).toEqual(mockCache) - expect(result.preview).toContain('configs:') - expect(result.cache_key).toBe('hub-bot-xyz789') - }) - - it('should encode special characters in slug', async () => { - const mockCache = { - preview: '# Custom', - cache_key: 'custom-key', - } - vi.mocked(client.get).mockResolvedValue({ data: mockCache }) - - await presets.getCrowdsecPresetCache('custom/preset with spaces') - - expect(client.get).toHaveBeenCalledWith( - '/admin/crowdsec/presets/cache/custom%2Fpreset%20with%20spaces' - ) - }) - - it('should handle cache miss (404)', async () => { - const error = { - response: { - status: 404, - data: { error: 'Preset not found in cache' }, - }, - } - vi.mocked(client.get).mockRejectedValue(error) - - await expect(presets.getCrowdsecPresetCache('non-cached-preset')).rejects.toEqual(error) - }) - - it('should handle expired cache entries', async () => { - const error = { - response: { - status: 410, - data: { error: 'Cache entry expired' }, - }, - } - vi.mocked(client.get).mockRejectedValue(error) - - await expect(presets.getCrowdsecPresetCache('expired-preset')).rejects.toEqual(error) - }) - - it('should handle empty preview content', async () => { - const mockCache = { - preview: '', - cache_key: 'empty-key', - } - vi.mocked(client.get).mockResolvedValue({ data: mockCache }) - - const result = await presets.getCrowdsecPresetCache('empty-preset') - - expect(result.preview).toBe('') - expect(result.cache_key).toBe('empty-key') - }) - }) - - describe('default export', () => { - it('should export all functions', () => { - expect(presets.default).toHaveProperty('listCrowdsecPresets') - expect(presets.default).toHaveProperty('getCrowdsecPresets') - expect(presets.default).toHaveProperty('pullCrowdsecPreset') - expect(presets.default).toHaveProperty('applyCrowdsecPreset') - expect(presets.default).toHaveProperty('getCrowdsecPresetCache') - }) - }) - - describe('integration scenarios', () => { - it('should handle full workflow: list → pull → cache → apply', async () => { - // 1. List presets - const mockList = { - presets: [ - { - slug: 'bot-mitigation-essentials', - title: 'Bot Mitigation', - summary: 'Core', - source: 'hub', - requires_hub: true, - available: true, - cached: false, - }, - ], - } - vi.mocked(client.get).mockResolvedValueOnce({ data: mockList }) - - const listResult = await presets.listCrowdsecPresets() - expect(listResult.presets[0].cached).toBe(false) - - // 2. Pull preset - const mockPull = { - status: 'success', - slug: 'bot-mitigation-essentials', - preview: '# Config', - cache_key: 'hub-bot-new123', - etag: '"etag1"', - retrieved_at: '2025-12-15T10:00:00Z', - } - vi.mocked(client.post).mockResolvedValueOnce({ data: mockPull }) - - const pullResult = await presets.pullCrowdsecPreset('bot-mitigation-essentials') - expect(pullResult.cache_key).toBe('hub-bot-new123') - - // 3. Verify cache - const mockCache = { - preview: '# Config', - cache_key: 'hub-bot-new123', - etag: '"etag1"', - } - vi.mocked(client.get).mockResolvedValueOnce({ data: mockCache }) - - const cacheResult = await presets.getCrowdsecPresetCache('bot-mitigation-essentials') - expect(cacheResult.cache_key).toBe(pullResult.cache_key) - - // 4. Apply preset - const mockApply = { - status: 'success', - backup: '/data/backups/preset-backup.tar.gz', - reload_hint: true, - cache_key: 'hub-bot-new123', - slug: 'bot-mitigation-essentials', - } - vi.mocked(client.post).mockResolvedValueOnce({ data: mockApply }) - - const applyResult = await presets.applyCrowdsecPreset({ - slug: 'bot-mitigation-essentials', - cache_key: pullResult.cache_key, - }) - expect(applyResult.status).toBe('success') - expect(applyResult.backup).toBeDefined() - }) - - it('should handle network failure mid-workflow', async () => { - // Pull succeeds - const mockPull = { - status: 'success', - slug: 'test-preset', - preview: '# Test', - cache_key: 'test-key', - } - vi.mocked(client.post).mockResolvedValueOnce({ data: mockPull }) - - const pullResult = await presets.pullCrowdsecPreset('test-preset') - expect(pullResult.cache_key).toBe('test-key') - - // Apply fails due to network - const networkError = new Error('Network error') - vi.mocked(client.post).mockRejectedValueOnce(networkError) - - await expect( - presets.applyCrowdsecPreset({ slug: 'test-preset', cache_key: 'test-key' }) - ).rejects.toThrow('Network error') - }) - }) -}) diff --git a/frontend/src/api/__tests__/proxyHosts-bulk.test.ts b/frontend/src/api/__tests__/proxyHosts-bulk.test.ts deleted file mode 100644 index e29cb091..00000000 --- a/frontend/src/api/__tests__/proxyHosts-bulk.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { bulkUpdateACL } from '../proxyHosts'; -import type { BulkUpdateACLResponse } from '../proxyHosts'; - -// Mock the client module -const mockPut = vi.fn(); -vi.mock('../client', () => ({ - default: { - put: (...args: unknown[]) => mockPut(...args), - }, -})); - -describe('proxyHosts bulk operations', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('bulkUpdateACL', () => { - it('should apply ACL to multiple hosts', async () => { - const mockResponse: BulkUpdateACLResponse = { - updated: 3, - errors: [], - }; - mockPut.mockResolvedValue({ data: mockResponse }); - - const hostUUIDs = ['uuid-1', 'uuid-2', 'uuid-3']; - const accessListID = 42; - const result = await bulkUpdateACL(hostUUIDs, accessListID); - - expect(mockPut).toHaveBeenCalledWith('/proxy-hosts/bulk-update-acl', { - host_uuids: hostUUIDs, - access_list_id: accessListID, - }); - expect(result).toEqual(mockResponse); - }); - - it('should remove ACL from hosts when accessListID is null', async () => { - const mockResponse: BulkUpdateACLResponse = { - updated: 2, - errors: [], - }; - mockPut.mockResolvedValue({ data: mockResponse }); - - const hostUUIDs = ['uuid-1', 'uuid-2']; - const result = await bulkUpdateACL(hostUUIDs, null); - - expect(mockPut).toHaveBeenCalledWith('/proxy-hosts/bulk-update-acl', { - host_uuids: hostUUIDs, - access_list_id: null, - }); - expect(result).toEqual(mockResponse); - }); - - it('should handle partial failures', async () => { - const mockResponse: BulkUpdateACLResponse = { - updated: 1, - errors: [ - { uuid: 'invalid-uuid', error: 'proxy host not found' }, - ], - }; - mockPut.mockResolvedValue({ data: mockResponse }); - - const hostUUIDs = ['valid-uuid', 'invalid-uuid']; - const accessListID = 10; - const result = await bulkUpdateACL(hostUUIDs, accessListID); - - expect(result.updated).toBe(1); - expect(result.errors).toHaveLength(1); - expect(result.errors[0].uuid).toBe('invalid-uuid'); - }); - - it('should handle empty host list', async () => { - const mockResponse: BulkUpdateACLResponse = { - updated: 0, - errors: [], - }; - mockPut.mockResolvedValue({ data: mockResponse }); - - const result = await bulkUpdateACL([], 5); - - expect(mockPut).toHaveBeenCalledWith('/proxy-hosts/bulk-update-acl', { - host_uuids: [], - access_list_id: 5, - }); - expect(result.updated).toBe(0); - }); - - it('should propagate API errors', async () => { - const error = new Error('Network error'); - mockPut.mockRejectedValue(error); - - await expect(bulkUpdateACL(['uuid-1'], 1)).rejects.toThrow('Network error'); - }); - }); -}); diff --git a/frontend/src/api/__tests__/proxyHosts.test.ts b/frontend/src/api/__tests__/proxyHosts.test.ts deleted file mode 100644 index 026d03af..00000000 --- a/frontend/src/api/__tests__/proxyHosts.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import client from '../client'; -import { - getProxyHosts, - getProxyHost, - createProxyHost, - updateProxyHost, - deleteProxyHost, - testProxyHostConnection, - ProxyHost -} from '../proxyHosts'; - -vi.mock('../client', () => ({ - default: { - get: vi.fn(), - post: vi.fn(), - put: vi.fn(), - delete: vi.fn(), - }, -})); - -describe('proxyHosts API', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - const mockHost: ProxyHost = { - uuid: '123', - name: 'Example Host', - domain_names: 'example.com', - forward_scheme: 'http', - forward_host: 'localhost', - forward_port: 8080, - ssl_forced: true, - http2_support: true, - hsts_enabled: true, - hsts_subdomains: false, - block_exploits: false, - websocket_support: false, - application: 'none', - locations: [], - enabled: true, - created_at: '2023-01-01', - updated_at: '2023-01-01', - }; - - it('getProxyHosts calls client.get', async () => { - vi.mocked(client.get).mockResolvedValue({ data: [mockHost] }); - const result = await getProxyHosts(); - expect(client.get).toHaveBeenCalledWith('/proxy-hosts'); - expect(result).toEqual([mockHost]); - }); - - it('getProxyHost calls client.get with uuid', async () => { - vi.mocked(client.get).mockResolvedValue({ data: mockHost }); - const result = await getProxyHost('123'); - expect(client.get).toHaveBeenCalledWith('/proxy-hosts/123'); - expect(result).toEqual(mockHost); - }); - - it('createProxyHost calls client.post', async () => { - vi.mocked(client.post).mockResolvedValue({ data: mockHost }); - const newHost = { domain_names: 'example.com' }; - const result = await createProxyHost(newHost); - expect(client.post).toHaveBeenCalledWith('/proxy-hosts', newHost); - expect(result).toEqual(mockHost); - }); - - it('updateProxyHost calls client.put', async () => { - vi.mocked(client.put).mockResolvedValue({ data: mockHost }); - const updates = { enabled: false }; - const result = await updateProxyHost('123', updates); - expect(client.put).toHaveBeenCalledWith('/proxy-hosts/123', updates); - expect(result).toEqual(mockHost); - }); - - it('deleteProxyHost calls client.delete', async () => { - vi.mocked(client.delete).mockResolvedValue({ data: {} }); - await deleteProxyHost('123'); - expect(client.delete).toHaveBeenCalledWith('/proxy-hosts/123'); - }); - - it('testProxyHostConnection calls client.post', async () => { - vi.mocked(client.post).mockResolvedValue({ data: {} }); - await testProxyHostConnection('localhost', 8080); - expect(client.post).toHaveBeenCalledWith('/proxy-hosts/test', { - forward_host: 'localhost', - forward_port: 8080, - }); - }); -}); diff --git a/frontend/src/api/__tests__/remoteServers.test.ts b/frontend/src/api/__tests__/remoteServers.test.ts deleted file mode 100644 index 84f5cd1c..00000000 --- a/frontend/src/api/__tests__/remoteServers.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { vi, describe, it, expect, beforeEach } from 'vitest'; -import { - getRemoteServers, - getRemoteServer, - createRemoteServer, - updateRemoteServer, - deleteRemoteServer, - testRemoteServerConnection, - testCustomRemoteServerConnection, -} from '../remoteServers'; -import client from '../client'; - -vi.mock('../client', () => ({ - default: { - get: vi.fn(), - post: vi.fn(), - put: vi.fn(), - delete: vi.fn(), - }, -})); - -describe('remoteServers API', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - const mockServer = { - uuid: 'server-123', - name: 'Test Server', - provider: 'docker', - host: '192.168.1.100', - port: 2375, - username: 'admin', - enabled: true, - reachable: true, - last_check: '2024-01-01T12:00:00Z', - created_at: '2024-01-01T00:00:00Z', - updated_at: '2024-01-01T12:00:00Z', - }; - - describe('getRemoteServers', () => { - it('fetches all servers', async () => { - vi.mocked(client.get).mockResolvedValue({ data: [mockServer] }); - - const result = await getRemoteServers(); - - expect(client.get).toHaveBeenCalledWith('/remote-servers', { params: {} }); - expect(result).toEqual([mockServer]); - }); - - it('fetches enabled servers only', async () => { - vi.mocked(client.get).mockResolvedValue({ data: [mockServer] }); - - const result = await getRemoteServers(true); - - expect(client.get).toHaveBeenCalledWith('/remote-servers', { params: { enabled: true } }); - expect(result).toEqual([mockServer]); - }); - }); - - describe('getRemoteServer', () => { - it('fetches a single server by UUID', async () => { - vi.mocked(client.get).mockResolvedValue({ data: mockServer }); - - const result = await getRemoteServer('server-123'); - - expect(client.get).toHaveBeenCalledWith('/remote-servers/server-123'); - expect(result).toEqual(mockServer); - }); - }); - - describe('createRemoteServer', () => { - it('creates a new server', async () => { - const newServer = { - name: 'New Server', - provider: 'docker', - host: '10.0.0.1', - port: 2375, - }; - vi.mocked(client.post).mockResolvedValue({ data: { ...mockServer, ...newServer } }); - - const result = await createRemoteServer(newServer); - - expect(client.post).toHaveBeenCalledWith('/remote-servers', newServer); - expect(result.name).toBe('New Server'); - }); - }); - - describe('updateRemoteServer', () => { - it('updates an existing server', async () => { - const updates = { name: 'Updated Server', enabled: false }; - vi.mocked(client.put).mockResolvedValue({ data: { ...mockServer, ...updates } }); - - const result = await updateRemoteServer('server-123', updates); - - expect(client.put).toHaveBeenCalledWith('/remote-servers/server-123', updates); - expect(result.name).toBe('Updated Server'); - expect(result.enabled).toBe(false); - }); - }); - - describe('deleteRemoteServer', () => { - it('deletes a server', async () => { - vi.mocked(client.delete).mockResolvedValue({}); - - await deleteRemoteServer('server-123'); - - expect(client.delete).toHaveBeenCalledWith('/remote-servers/server-123'); - }); - }); - - describe('testRemoteServerConnection', () => { - it('tests connection to an existing server', async () => { - vi.mocked(client.post).mockResolvedValue({ data: { address: '192.168.1.100:2375' } }); - - const result = await testRemoteServerConnection('server-123'); - - expect(client.post).toHaveBeenCalledWith('/remote-servers/server-123/test'); - expect(result.address).toBe('192.168.1.100:2375'); - }); - }); - - describe('testCustomRemoteServerConnection', () => { - it('tests connection to a custom host and port', async () => { - vi.mocked(client.post).mockResolvedValue({ - data: { address: '10.0.0.1:2375', reachable: true }, - }); - - const result = await testCustomRemoteServerConnection('10.0.0.1', 2375); - - expect(client.post).toHaveBeenCalledWith('/remote-servers/test', { host: '10.0.0.1', port: 2375 }); - expect(result.reachable).toBe(true); - }); - - it('handles unreachable server', async () => { - vi.mocked(client.post).mockResolvedValue({ - data: { address: '10.0.0.1:2375', reachable: false, error: 'Connection refused' }, - }); - - const result = await testCustomRemoteServerConnection('10.0.0.1', 2375); - - expect(result.reachable).toBe(false); - expect(result.error).toBe('Connection refused'); - }); - }); -}); diff --git a/frontend/src/api/__tests__/security.test.ts b/frontend/src/api/__tests__/security.test.ts deleted file mode 100644 index f548eb21..00000000 --- a/frontend/src/api/__tests__/security.test.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' -import * as security from '../security' -import client from '../client' - -vi.mock('../client') - -describe('security API', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - describe('getSecurityStatus', () => { - it('should call GET /security/status', async () => { - const mockData: security.SecurityStatus = { - cerberus: { enabled: true }, - crowdsec: { mode: 'local', api_url: 'http://localhost:8080', enabled: true }, - waf: { mode: 'enabled', enabled: true }, - rate_limit: { mode: 'enabled', enabled: true }, - acl: { enabled: true } - } - vi.mocked(client.get).mockResolvedValue({ data: mockData }) - - const result = await security.getSecurityStatus() - - expect(client.get).toHaveBeenCalledWith('/security/status') - expect(result).toEqual(mockData) - }) - }) - - describe('getSecurityConfig', () => { - it('should call GET /security/config', async () => { - const mockData = { config: { admin_whitelist: '10.0.0.0/8' } } - vi.mocked(client.get).mockResolvedValue({ data: mockData }) - - const result = await security.getSecurityConfig() - - expect(client.get).toHaveBeenCalledWith('/security/config') - expect(result).toEqual(mockData) - }) - }) - - describe('updateSecurityConfig', () => { - it('should call POST /security/config with payload', async () => { - const payload: security.SecurityConfigPayload = { - name: 'test', - enabled: true, - admin_whitelist: '10.0.0.0/8' - } - const mockData = { success: true } - vi.mocked(client.post).mockResolvedValue({ data: mockData }) - - const result = await security.updateSecurityConfig(payload) - - expect(client.post).toHaveBeenCalledWith('/security/config', payload) - expect(result).toEqual(mockData) - }) - - it('should handle all payload fields', async () => { - const payload: security.SecurityConfigPayload = { - name: 'test', - enabled: true, - admin_whitelist: '10.0.0.0/8', - crowdsec_mode: 'local', - crowdsec_api_url: 'http://localhost:8080', - waf_mode: 'enabled', - waf_rules_source: 'coreruleset', - waf_learning: true, - rate_limit_enable: true, - rate_limit_burst: 10, - rate_limit_requests: 100, - rate_limit_window_sec: 60 - } - const mockData = { success: true } - vi.mocked(client.post).mockResolvedValue({ data: mockData }) - - const result = await security.updateSecurityConfig(payload) - - expect(client.post).toHaveBeenCalledWith('/security/config', payload) - expect(result).toEqual(mockData) - }) - }) - - describe('generateBreakGlassToken', () => { - it('should call POST /security/breakglass/generate', async () => { - const mockData = { token: 'abc123' } - vi.mocked(client.post).mockResolvedValue({ data: mockData }) - - const result = await security.generateBreakGlassToken() - - expect(client.post).toHaveBeenCalledWith('/security/breakglass/generate') - expect(result).toEqual(mockData) - }) - }) - - describe('enableCerberus', () => { - it('should call POST /security/enable with payload', async () => { - const payload = { mode: 'full' } - const mockData = { success: true } - vi.mocked(client.post).mockResolvedValue({ data: mockData }) - - const result = await security.enableCerberus(payload) - - expect(client.post).toHaveBeenCalledWith('/security/enable', payload) - expect(result).toEqual(mockData) - }) - - it('should call POST /security/enable with empty object when no payload', async () => { - const mockData = { success: true } - vi.mocked(client.post).mockResolvedValue({ data: mockData }) - - const result = await security.enableCerberus() - - expect(client.post).toHaveBeenCalledWith('/security/enable', {}) - expect(result).toEqual(mockData) - }) - }) - - describe('disableCerberus', () => { - it('should call POST /security/disable with payload', async () => { - const payload = { reason: 'maintenance' } - const mockData = { success: true } - vi.mocked(client.post).mockResolvedValue({ data: mockData }) - - const result = await security.disableCerberus(payload) - - expect(client.post).toHaveBeenCalledWith('/security/disable', payload) - expect(result).toEqual(mockData) - }) - - it('should call POST /security/disable with empty object when no payload', async () => { - const mockData = { success: true } - vi.mocked(client.post).mockResolvedValue({ data: mockData }) - - const result = await security.disableCerberus() - - expect(client.post).toHaveBeenCalledWith('/security/disable', {}) - expect(result).toEqual(mockData) - }) - }) - - describe('getDecisions', () => { - it('should call GET /security/decisions with default limit', async () => { - const mockData = { decisions: [] } - vi.mocked(client.get).mockResolvedValue({ data: mockData }) - - const result = await security.getDecisions() - - expect(client.get).toHaveBeenCalledWith('/security/decisions?limit=50') - expect(result).toEqual(mockData) - }) - - it('should call GET /security/decisions with custom limit', async () => { - const mockData = { decisions: [] } - vi.mocked(client.get).mockResolvedValue({ data: mockData }) - - const result = await security.getDecisions(100) - - expect(client.get).toHaveBeenCalledWith('/security/decisions?limit=100') - expect(result).toEqual(mockData) - }) - }) - - describe('createDecision', () => { - it('should call POST /security/decisions with payload', async () => { - const payload = { value: '1.2.3.4', duration: '4h', type: 'ban' } - const mockData = { success: true } - vi.mocked(client.post).mockResolvedValue({ data: mockData }) - - const result = await security.createDecision(payload) - - expect(client.post).toHaveBeenCalledWith('/security/decisions', payload) - expect(result).toEqual(mockData) - }) - }) - - describe('getRuleSets', () => { - it('should call GET /security/rulesets', async () => { - const mockData: security.RuleSetsResponse = { - rulesets: [ - { - id: 1, - uuid: 'abc-123', - name: 'OWASP CRS', - source_url: 'https://example.com/rules', - mode: 'blocking', - last_updated: '2025-12-04T00:00:00Z', - content: 'rule content' - } - ] - } - vi.mocked(client.get).mockResolvedValue({ data: mockData }) - - const result = await security.getRuleSets() - - expect(client.get).toHaveBeenCalledWith('/security/rulesets') - expect(result).toEqual(mockData) - }) - }) - - describe('upsertRuleSet', () => { - it('should call POST /security/rulesets with create payload', async () => { - const payload: security.UpsertRuleSetPayload = { - name: 'Custom Rules', - content: 'rule content', - mode: 'blocking' - } - const mockData = { success: true } - vi.mocked(client.post).mockResolvedValue({ data: mockData }) - - const result = await security.upsertRuleSet(payload) - - expect(client.post).toHaveBeenCalledWith('/security/rulesets', payload) - expect(result).toEqual(mockData) - }) - - it('should call POST /security/rulesets with update payload', async () => { - const payload: security.UpsertRuleSetPayload = { - id: 1, - name: 'Updated Rules', - source_url: 'https://example.com/rules', - mode: 'detection' - } - const mockData = { success: true } - vi.mocked(client.post).mockResolvedValue({ data: mockData }) - - const result = await security.upsertRuleSet(payload) - - expect(client.post).toHaveBeenCalledWith('/security/rulesets', payload) - expect(result).toEqual(mockData) - }) - }) - - describe('deleteRuleSet', () => { - it('should call DELETE /security/rulesets/:id', async () => { - const mockData = { success: true } - vi.mocked(client.delete).mockResolvedValue({ data: mockData }) - - const result = await security.deleteRuleSet(1) - - expect(client.delete).toHaveBeenCalledWith('/security/rulesets/1') - expect(result).toEqual(mockData) - }) - }) -}) diff --git a/frontend/src/api/__tests__/settings.test.ts b/frontend/src/api/__tests__/settings.test.ts deleted file mode 100644 index 257e8ffc..00000000 --- a/frontend/src/api/__tests__/settings.test.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' -import * as settings from '../settings' -import client from '../client' - -vi.mock('../client') - -describe('settings API', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - describe('getSettings', () => { - it('should call GET /settings', async () => { - const mockData: settings.SettingsMap = { - 'ui.theme': 'dark', - 'security.cerberus.enabled': 'true' - } - vi.mocked(client.get).mockResolvedValue({ data: mockData }) - - const result = await settings.getSettings() - - expect(client.get).toHaveBeenCalledWith('/settings') - expect(result).toEqual(mockData) - }) - }) - - describe('updateSetting', () => { - it('should call POST /settings with key and value only', async () => { - vi.mocked(client.post).mockResolvedValue({ data: {} }) - - await settings.updateSetting('ui.theme', 'light') - - expect(client.post).toHaveBeenCalledWith('/settings', { - key: 'ui.theme', - value: 'light', - category: undefined, - type: undefined - }) - }) - - it('should call POST /settings with all parameters', async () => { - vi.mocked(client.post).mockResolvedValue({ data: {} }) - - await settings.updateSetting('security.cerberus.enabled', 'true', 'security', 'bool') - - expect(client.post).toHaveBeenCalledWith('/settings', { - key: 'security.cerberus.enabled', - value: 'true', - category: 'security', - type: 'bool' - }) - }) - - it('should call POST /settings with category but no type', async () => { - vi.mocked(client.post).mockResolvedValue({ data: {} }) - - await settings.updateSetting('ui.theme', 'dark', 'ui') - - expect(client.post).toHaveBeenCalledWith('/settings', { - key: 'ui.theme', - value: 'dark', - category: 'ui', - type: undefined - }) - }) - }) - - describe('validatePublicURL', () => { - it('should call POST /settings/validate-url with URL', async () => { - const mockResponse = { valid: true, normalized: 'https://example.com' } - vi.mocked(client.post).mockResolvedValue({ data: mockResponse }) - - const result = await settings.validatePublicURL('https://example.com') - - expect(client.post).toHaveBeenCalledWith('/settings/validate-url', { url: 'https://example.com' }) - expect(result).toEqual(mockResponse) - }) - - it('should return valid: true for valid URL', async () => { - vi.mocked(client.post).mockResolvedValue({ data: { valid: true } }) - - const result = await settings.validatePublicURL('https://valid.com') - - expect(result.valid).toBe(true) - }) - - it('should return valid: false for invalid URL', async () => { - vi.mocked(client.post).mockResolvedValue({ data: { valid: false, error: 'Invalid URL format' } }) - - const result = await settings.validatePublicURL('not-a-url') - - expect(result.valid).toBe(false) - expect(result.error).toBe('Invalid URL format') - }) - - it('should return normalized URL when provided', async () => { - vi.mocked(client.post).mockResolvedValue({ - data: { valid: true, normalized: 'https://example.com/' } - }) - - const result = await settings.validatePublicURL('https://example.com') - - expect(result.normalized).toBe('https://example.com/') - }) - - it('should handle validation errors', async () => { - vi.mocked(client.post).mockRejectedValue(new Error('Network error')) - - await expect(settings.validatePublicURL('https://example.com')).rejects.toThrow('Network error') - }) - - it('should handle empty URL parameter', async () => { - vi.mocked(client.post).mockResolvedValue({ data: { valid: false } }) - - const result = await settings.validatePublicURL('') - - expect(client.post).toHaveBeenCalledWith('/settings/validate-url', { url: '' }) - expect(result.valid).toBe(false) - }) - }) - - describe('testPublicURL', () => { - it('should call POST /settings/test-url with URL', async () => { - const mockResponse = { reachable: true, latency: 42 } - vi.mocked(client.post).mockResolvedValue({ data: mockResponse }) - - const result = await settings.testPublicURL('https://example.com') - - expect(client.post).toHaveBeenCalledWith('/settings/test-url', { url: 'https://example.com' }) - expect(result).toEqual(mockResponse) - }) - - it('should return reachable: true with latency for successful test', async () => { - vi.mocked(client.post).mockResolvedValue({ - data: { reachable: true, latency: 123, message: 'URL is reachable' } - }) - - const result = await settings.testPublicURL('https://example.com') - - expect(result.reachable).toBe(true) - expect(result.latency).toBe(123) - expect(result.message).toBe('URL is reachable') - }) - - it('should return reachable: false with error for failed test', async () => { - vi.mocked(client.post).mockResolvedValue({ - data: { reachable: false, error: 'Connection timeout' } - }) - - const result = await settings.testPublicURL('https://unreachable.com') - - expect(result.reachable).toBe(false) - expect(result.error).toBe('Connection timeout') - }) - - it('should return message field when provided', async () => { - vi.mocked(client.post).mockResolvedValue({ - data: { reachable: true, latency: 50, message: 'Custom success message' } - }) - - const result = await settings.testPublicURL('https://example.com') - - expect(result.message).toBe('Custom success message') - }) - - it('should handle request errors', async () => { - vi.mocked(client.post).mockRejectedValue(new Error('Request failed')) - - await expect(settings.testPublicURL('https://example.com')).rejects.toThrow('Request failed') - }) - - it('should handle empty URL parameter', async () => { - vi.mocked(client.post).mockResolvedValue({ data: { reachable: false } }) - - const result = await settings.testPublicURL('') - - expect(client.post).toHaveBeenCalledWith('/settings/test-url', { url: '' }) - expect(result.reachable).toBe(false) - }) - }) -}) diff --git a/frontend/src/api/__tests__/setup.test.ts b/frontend/src/api/__tests__/setup.test.ts deleted file mode 100644 index c2c633a5..00000000 --- a/frontend/src/api/__tests__/setup.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' -import client from '../../api/client' -import { getSetupStatus, performSetup } from '../setup' - -describe('setup api', () => { - beforeEach(() => { - vi.restoreAllMocks() - }) - - it('getSetupStatus returns status', async () => { - const data = { setupRequired: true } - vi.spyOn(client, 'get').mockResolvedValueOnce({ data }) - const res = await getSetupStatus() - expect(res).toEqual(data) - }) - - it('performSetup posts data to setup endpoint', async () => { - const spy = vi.spyOn(client, 'post').mockResolvedValueOnce({ data: {} }) - const payload = { name: 'Admin', email: 'admin@example.com', password: 'secret' } - await performSetup(payload) - expect(spy).toHaveBeenCalledWith('/setup', payload) - }) -}) diff --git a/frontend/src/api/__tests__/system.test.ts b/frontend/src/api/__tests__/system.test.ts deleted file mode 100644 index 1b7a8891..00000000 --- a/frontend/src/api/__tests__/system.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { describe, it, expect, vi, afterEach } from 'vitest' -import client from '../client' -import { checkUpdates, getNotifications, markNotificationRead, markAllNotificationsRead } from '../system' - -vi.mock('../client', () => ({ - default: { - get: vi.fn(), - post: vi.fn(), - }, -})) - -describe('System API', () => { - afterEach(() => { - vi.clearAllMocks() - }) - - it('checkUpdates calls /system/updates', async () => { - const mockData = { available: true, latest_version: '1.0.0', changelog_url: 'url' } - vi.mocked(client.get).mockResolvedValue({ data: mockData }) - - const result = await checkUpdates() - - expect(client.get).toHaveBeenCalledWith('/system/updates') - expect(result).toEqual(mockData) - }) - - it('getNotifications calls /notifications', async () => { - const mockData = [{ id: '1', title: 'Test' }] - vi.mocked(client.get).mockResolvedValue({ data: mockData }) - - const result = await getNotifications() - - expect(client.get).toHaveBeenCalledWith('/notifications', { params: { unread: false } }) - expect(result).toEqual(mockData) - }) - - it('getNotifications calls /notifications with unreadOnly=true', async () => { - const mockData = [{ id: '1', title: 'Test' }] - vi.mocked(client.get).mockResolvedValue({ data: mockData }) - - const result = await getNotifications(true) - - expect(client.get).toHaveBeenCalledWith('/notifications', { params: { unread: true } }) - expect(result).toEqual(mockData) - }) - - it('markNotificationRead calls /notifications/:id/read', async () => { - vi.mocked(client.post).mockResolvedValue({}) - - await markNotificationRead('123') - - expect(client.post).toHaveBeenCalledWith('/notifications/123/read') - }) - - it('markAllNotificationsRead calls /notifications/read-all', async () => { - vi.mocked(client.post).mockResolvedValue({}) - - await markAllNotificationsRead() - - expect(client.post).toHaveBeenCalledWith('/notifications/read-all') - }) -}) diff --git a/frontend/src/api/__tests__/uptime.test.ts b/frontend/src/api/__tests__/uptime.test.ts deleted file mode 100644 index d11affa9..00000000 --- a/frontend/src/api/__tests__/uptime.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' -import * as uptime from '../uptime' -import client from '../client' -import type { UptimeMonitor, UptimeHeartbeat } from '../uptime' - -vi.mock('../client') - -describe('uptime API', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - describe('getMonitors', () => { - it('should call GET /uptime/monitors', async () => { - const mockData: UptimeMonitor[] = [ - { - id: 'mon-1', - name: 'Test Monitor', - type: 'http', - url: 'https://example.com', - interval: 60, - enabled: true, - status: 'up', - latency: 100, - max_retries: 3 - } - ] - vi.mocked(client.get).mockResolvedValue({ data: mockData }) - - const result = await uptime.getMonitors() - - expect(client.get).toHaveBeenCalledWith('/uptime/monitors') - expect(result).toEqual(mockData) - }) - }) - - describe('getMonitorHistory', () => { - it('should call GET /uptime/monitors/:id/history with default limit', async () => { - const mockData: UptimeHeartbeat[] = [ - { - id: 1, - monitor_id: 'mon-1', - status: 'up', - latency: 100, - message: 'OK', - created_at: '2025-12-04T00:00:00Z' - } - ] - vi.mocked(client.get).mockResolvedValue({ data: mockData }) - - const result = await uptime.getMonitorHistory('mon-1') - - expect(client.get).toHaveBeenCalledWith('/uptime/monitors/mon-1/history?limit=50') - expect(result).toEqual(mockData) - }) - - it('should call GET /uptime/monitors/:id/history with custom limit', async () => { - const mockData: UptimeHeartbeat[] = [] - vi.mocked(client.get).mockResolvedValue({ data: mockData }) - - const result = await uptime.getMonitorHistory('mon-1', 100) - - expect(client.get).toHaveBeenCalledWith('/uptime/monitors/mon-1/history?limit=100') - expect(result).toEqual(mockData) - }) - }) - - describe('updateMonitor', () => { - it('should call PUT /uptime/monitors/:id', async () => { - const mockMonitor: UptimeMonitor = { - id: 'mon-1', - name: 'Updated Monitor', - type: 'http', - url: 'https://example.com', - interval: 120, - enabled: false, - status: 'down', - latency: 0, - max_retries: 5 - } - vi.mocked(client.put).mockResolvedValue({ data: mockMonitor }) - - const result = await uptime.updateMonitor('mon-1', { enabled: false, interval: 120 }) - - expect(client.put).toHaveBeenCalledWith('/uptime/monitors/mon-1', { enabled: false, interval: 120 }) - expect(result).toEqual(mockMonitor) - }) - }) - - describe('deleteMonitor', () => { - it('should call DELETE /uptime/monitors/:id', async () => { - vi.mocked(client.delete).mockResolvedValue({ data: undefined }) - - const result = await uptime.deleteMonitor('mon-1') - - expect(client.delete).toHaveBeenCalledWith('/uptime/monitors/mon-1') - expect(result).toBeUndefined() - }) - }) - - describe('syncMonitors', () => { - it('should call POST /uptime/sync with empty body when no params', async () => { - const mockData = { synced: 5 } - vi.mocked(client.post).mockResolvedValue({ data: mockData }) - - const result = await uptime.syncMonitors() - - expect(client.post).toHaveBeenCalledWith('/uptime/sync', {}) - expect(result).toEqual(mockData) - }) - - it('should call POST /uptime/sync with provided parameters', async () => { - const mockData = { synced: 5 } - const body = { interval: 120, max_retries: 5 } - vi.mocked(client.post).mockResolvedValue({ data: mockData }) - - const result = await uptime.syncMonitors(body) - - expect(client.post).toHaveBeenCalledWith('/uptime/sync', body) - expect(result).toEqual(mockData) - }) - }) - - describe('checkMonitor', () => { - it('should call POST /uptime/monitors/:id/check', async () => { - const mockData = { message: 'Check initiated' } - vi.mocked(client.post).mockResolvedValue({ data: mockData }) - - const result = await uptime.checkMonitor('mon-1') - - expect(client.post).toHaveBeenCalledWith('/uptime/monitors/mon-1/check') - expect(result).toEqual(mockData) - }) - }) -}) diff --git a/frontend/src/api/__tests__/users.test.ts b/frontend/src/api/__tests__/users.test.ts deleted file mode 100644 index f66d317a..00000000 --- a/frontend/src/api/__tests__/users.test.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' -import client from '../client' -import { - listUsers, - getUser, - createUser, - inviteUser, - updateUser, - deleteUser, - updateUserPermissions, - validateInvite, - acceptInvite, -} from '../users' - -vi.mock('../client', () => ({ - default: { - get: vi.fn(), - post: vi.fn(), - put: vi.fn(), - delete: vi.fn(), - }, -})) - -describe('users api', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it('lists, reads, creates, updates, and deletes users', async () => { - vi.mocked(client.get).mockResolvedValueOnce({ data: [{ id: 1, email: 'a' }] }) - const users = await listUsers() - expect(users[0].id).toBe(1) - expect(client.get).toHaveBeenCalledWith('/users') - - vi.mocked(client.get).mockResolvedValueOnce({ data: { id: 2 } }) - await getUser(2) - expect(client.get).toHaveBeenCalledWith('/users/2') - - vi.mocked(client.post).mockResolvedValueOnce({ data: { id: 3 } }) - await createUser({ email: 'e', name: 'n', password: 'p' }) - expect(client.post).toHaveBeenCalledWith('/users', { email: 'e', name: 'n', password: 'p' }) - - vi.mocked(client.put).mockResolvedValueOnce({ data: { message: 'ok' } }) - await updateUser(2, { enabled: false }) - expect(client.put).toHaveBeenCalledWith('/users/2', { enabled: false }) - - vi.mocked(client.delete).mockResolvedValueOnce({ data: { message: 'deleted' } }) - await deleteUser(2) - expect(client.delete).toHaveBeenCalledWith('/users/2') - }) - - it('invites users and updates permissions', async () => { - vi.mocked(client.post).mockResolvedValueOnce({ data: { invite_token: 't' } }) - await inviteUser({ email: 'i', permission_mode: 'allow_all' }) - expect(client.post).toHaveBeenCalledWith('/users/invite', { email: 'i', permission_mode: 'allow_all' }) - - vi.mocked(client.put).mockResolvedValueOnce({ data: { message: 'saved' } }) - await updateUserPermissions(1, { permission_mode: 'deny_all', permitted_hosts: [1, 2] }) - expect(client.put).toHaveBeenCalledWith('/users/1/permissions', { permission_mode: 'deny_all', permitted_hosts: [1, 2] }) - }) - - it('validates and accepts invites with params', async () => { - vi.mocked(client.get).mockResolvedValueOnce({ data: { valid: true, email: 'a' } }) - await validateInvite('token-1') - expect(client.get).toHaveBeenCalledWith('/invite/validate', { params: { token: 'token-1' } }) - - vi.mocked(client.post).mockResolvedValueOnce({ data: { message: 'accepted', email: 'a' } }) - await acceptInvite({ token: 't', name: 'n', password: 'p' }) - expect(client.post).toHaveBeenCalledWith('/invite/accept', { token: 't', name: 'n', password: 'p' }) - }) - - describe('previewInviteURL', () => { - it('should call POST /users/preview-invite-url with email', async () => { - const mockResponse = { - preview_url: 'https://example.com/accept-invite?token=SAMPLE_TOKEN_PREVIEW', - base_url: 'https://example.com', - is_configured: true, - email: 'test@example.com', - warning: false, - warning_message: '' - } - vi.mocked(client.post).mockResolvedValue({ data: mockResponse }) - - const result = await import('../users').then(m => m.previewInviteURL('test@example.com')) - - expect(client.post).toHaveBeenCalledWith('/users/preview-invite-url', { email: 'test@example.com' }) - expect(result).toEqual(mockResponse) - }) - - it('should return complete PreviewInviteURLResponse structure', async () => { - const mockResponse = { - preview_url: 'https://charon.example.com/accept-invite?token=SAMPLE_TOKEN_PREVIEW', - base_url: 'https://charon.example.com', - is_configured: true, - email: 'user@test.com', - warning: false, - warning_message: '' - } - vi.mocked(client.post).mockResolvedValue({ data: mockResponse }) - - const result = await import('../users').then(m => m.previewInviteURL('user@test.com')) - - expect(result.preview_url).toBeDefined() - expect(result.base_url).toBeDefined() - expect(result.is_configured).toBeDefined() - expect(result.email).toBeDefined() - expect(result.warning).toBeDefined() - expect(result.warning_message).toBeDefined() - }) - - it('should return preview_url with sample token', async () => { - vi.mocked(client.post).mockResolvedValue({ - data: { - preview_url: 'http://localhost:8080/accept-invite?token=SAMPLE_TOKEN_PREVIEW', - base_url: 'http://localhost:8080', - is_configured: false, - email: 'test@example.com', - warning: true, - warning_message: 'Public URL not configured' - } - }) - - const result = await import('../users').then(m => m.previewInviteURL('test@example.com')) - - expect(result.preview_url).toContain('SAMPLE_TOKEN_PREVIEW') - }) - - it('should return is_configured flag', async () => { - vi.mocked(client.post).mockResolvedValue({ - data: { - preview_url: 'https://example.com/accept-invite?token=SAMPLE_TOKEN_PREVIEW', - base_url: 'https://example.com', - is_configured: true, - email: 'test@example.com', - warning: false, - warning_message: '' - } - }) - - const result = await import('../users').then(m => m.previewInviteURL('test@example.com')) - - expect(result.is_configured).toBe(true) - }) - - it('should return warning flag when public URL not configured', async () => { - vi.mocked(client.post).mockResolvedValue({ - data: { - preview_url: 'http://localhost:8080/accept-invite?token=SAMPLE_TOKEN_PREVIEW', - base_url: 'http://localhost:8080', - is_configured: false, - email: 'admin@test.com', - warning: true, - warning_message: 'Using default localhost URL' - } - }) - - const result = await import('../users').then(m => m.previewInviteURL('admin@test.com')) - - expect(result.warning).toBe(true) - expect(result.warning_message).toBe('Using default localhost URL') - }) - - it('should return the provided email in response', async () => { - const testEmail = 'specific@email.com' - vi.mocked(client.post).mockResolvedValue({ - data: { - preview_url: 'https://example.com/accept-invite?token=SAMPLE_TOKEN_PREVIEW', - base_url: 'https://example.com', - is_configured: true, - email: testEmail, - warning: false, - warning_message: '' - } - }) - - const result = await import('../users').then(m => m.previewInviteURL(testEmail)) - - expect(result.email).toBe(testEmail) - }) - - it('should handle request errors', async () => { - vi.mocked(client.post).mockRejectedValue(new Error('Network error')) - - await expect( - import('../users').then(m => m.previewInviteURL('test@example.com')) - ).rejects.toThrow('Network error') - }) - }) -}) diff --git a/frontend/src/api/__tests__/websocket.test.ts b/frontend/src/api/__tests__/websocket.test.ts deleted file mode 100644 index cc3cf9b6..00000000 --- a/frontend/src/api/__tests__/websocket.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { getWebSocketConnections, getWebSocketStats } from '../websocket'; -import client from '../client'; - -vi.mock('../client'); - -describe('WebSocket API', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('getWebSocketConnections', () => { - it('should fetch WebSocket connections', async () => { - const mockResponse = { - connections: [ - { - id: 'test-conn-1', - type: 'logs', - connected_at: '2024-01-15T10:00:00Z', - last_activity_at: '2024-01-15T10:05:00Z', - remote_addr: '192.168.1.1:12345', - user_agent: 'Mozilla/5.0', - filters: 'level=error', - }, - { - id: 'test-conn-2', - type: 'cerberus', - connected_at: '2024-01-15T10:02:00Z', - last_activity_at: '2024-01-15T10:06:00Z', - remote_addr: '192.168.1.2:54321', - user_agent: 'Chrome/90.0', - filters: 'source=waf', - }, - ], - count: 2, - }; - - vi.mocked(client.get).mockResolvedValue({ data: mockResponse }); - - const result = await getWebSocketConnections(); - - expect(client.get).toHaveBeenCalledWith('/websocket/connections'); - expect(result).toEqual(mockResponse); - expect(result.count).toBe(2); - expect(result.connections).toHaveLength(2); - }); - - it('should handle empty connections', async () => { - const mockResponse = { - connections: [], - count: 0, - }; - - vi.mocked(client.get).mockResolvedValue({ data: mockResponse }); - - const result = await getWebSocketConnections(); - - expect(result.connections).toHaveLength(0); - expect(result.count).toBe(0); - }); - - it('should handle API errors', async () => { - vi.mocked(client.get).mockRejectedValue(new Error('Network error')); - - await expect(getWebSocketConnections()).rejects.toThrow('Network error'); - }); - }); - - describe('getWebSocketStats', () => { - it('should fetch WebSocket statistics', async () => { - const mockResponse = { - total_active: 3, - logs_connections: 2, - cerberus_connections: 1, - oldest_connection: '2024-01-15T09:55:00Z', - last_updated: '2024-01-15T10:10:00Z', - }; - - vi.mocked(client.get).mockResolvedValue({ data: mockResponse }); - - const result = await getWebSocketStats(); - - expect(client.get).toHaveBeenCalledWith('/websocket/stats'); - expect(result).toEqual(mockResponse); - expect(result.total_active).toBe(3); - expect(result.logs_connections).toBe(2); - expect(result.cerberus_connections).toBe(1); - }); - - it('should handle stats with no connections', async () => { - const mockResponse = { - total_active: 0, - logs_connections: 0, - cerberus_connections: 0, - last_updated: '2024-01-15T10:10:00Z', - }; - - vi.mocked(client.get).mockResolvedValue({ data: mockResponse }); - - const result = await getWebSocketStats(); - - expect(result.total_active).toBe(0); - expect(result.oldest_connection).toBeUndefined(); - }); - - it('should handle API errors', async () => { - vi.mocked(client.get).mockRejectedValue(new Error('Server error')); - - await expect(getWebSocketStats()).rejects.toThrow('Server error'); - }); - }); -}); diff --git a/frontend/src/api/accessLists.ts b/frontend/src/api/accessLists.ts deleted file mode 100644 index ba4b13fe..00000000 --- a/frontend/src/api/accessLists.ts +++ /dev/null @@ -1,126 +0,0 @@ -import client from './client'; - -export interface AccessListRule { - cidr: string; - description: string; -} - -export interface AccessList { - id: number; - uuid: string; - name: string; - description: string; - type: 'whitelist' | 'blacklist' | 'geo_whitelist' | 'geo_blacklist'; - ip_rules: string; // JSON string of AccessListRule[] - country_codes: string; // Comma-separated - local_network_only: boolean; - enabled: boolean; - created_at: string; - updated_at: string; -} - -export interface CreateAccessListRequest { - name: string; - description?: string; - type: 'whitelist' | 'blacklist' | 'geo_whitelist' | 'geo_blacklist'; - ip_rules?: string; - country_codes?: string; - local_network_only?: boolean; - enabled?: boolean; -} - -export interface TestIPRequest { - ip_address: string; -} - -export interface TestIPResponse { - allowed: boolean; - reason: string; -} - -export interface AccessListTemplate { - name: string; - description: string; - type: string; - local_network_only?: boolean; - country_codes?: string; -} - -export const accessListsApi = { - /** - * Fetches all access lists. - * @returns Promise resolving to array of AccessList objects - * @throws {AxiosError} If the request fails - */ - async list(): Promise { - const response = await client.get('/access-lists'); - return response.data; - }, - - /** - * Gets a single access list by ID. - * @param id - The access list ID - * @returns Promise resolving to the AccessList object - * @throws {AxiosError} If the request fails or access list not found - */ - async get(id: number): Promise { - const response = await client.get(`/access-lists/${id}`); - return response.data; - }, - - /** - * Creates a new access list. - * @param data - CreateAccessListRequest with access list configuration - * @returns Promise resolving to the created AccessList - * @throws {AxiosError} If creation fails or validation errors occur - */ - async create(data: CreateAccessListRequest): Promise { - const response = await client.post('/access-lists', data); - return response.data; - }, - - /** - * Updates an existing access list. - * @param id - The access list ID to update - * @param data - Partial CreateAccessListRequest with fields to update - * @returns Promise resolving to the updated AccessList - * @throws {AxiosError} If update fails or access list not found - */ - async update(id: number, data: Partial): Promise { - const response = await client.put(`/access-lists/${id}`, data); - return response.data; - }, - - /** - * Deletes an access list. - * @param id - The access list ID to delete - * @throws {AxiosError} If deletion fails or access list not found - */ - async delete(id: number): Promise { - await client.delete(`/access-lists/${id}`); - }, - - /** - * Tests if an IP address would be allowed or blocked by an access list. - * @param id - The access list ID to test against - * @param ipAddress - The IP address to test - * @returns Promise resolving to TestIPResponse with allowed status and reason - * @throws {AxiosError} If test fails or access list not found - */ - async testIP(id: number, ipAddress: string): Promise { - const response = await client.post(`/access-lists/${id}/test`, { - ip_address: ipAddress, - }); - return response.data; - }, - - /** - * Gets predefined access list templates. - * @returns Promise resolving to array of AccessListTemplate objects - * @throws {AxiosError} If the request fails - */ - async getTemplates(): Promise { - const response = await client.get('/access-lists/templates'); - return response.data; - }, -}; diff --git a/frontend/src/api/backups.ts b/frontend/src/api/backups.ts deleted file mode 100644 index 31550604..00000000 --- a/frontend/src/api/backups.ts +++ /dev/null @@ -1,46 +0,0 @@ -import client from './client'; - -/** Represents a backup file stored on the server. */ -export interface BackupFile { - filename: string; - size: number; - time: string; -} - -/** - * Fetches all available backup files. - * @returns Promise resolving to array of BackupFile objects - * @throws {AxiosError} If the request fails - */ -export const getBackups = async (): Promise => { - const response = await client.get('/backups'); - return response.data; -}; - -/** - * Creates a new backup of the current configuration. - * @returns Promise resolving to object containing the new backup filename - * @throws {AxiosError} If backup creation fails - */ -export const createBackup = async (): Promise<{ filename: string }> => { - const response = await client.post<{ filename: string }>('/backups'); - return response.data; -}; - -/** - * Restores configuration from a backup file. - * @param filename - The name of the backup file to restore - * @throws {AxiosError} If restoration fails or file not found - */ -export const restoreBackup = async (filename: string): Promise => { - await client.post(`/backups/${filename}/restore`); -}; - -/** - * Deletes a backup file. - * @param filename - The name of the backup file to delete - * @throws {AxiosError} If deletion fails or file not found - */ -export const deleteBackup = async (filename: string): Promise => { - await client.delete(`/backups/${filename}`); -}; diff --git a/frontend/src/api/certificates.ts b/frontend/src/api/certificates.ts deleted file mode 100644 index 154726ee..00000000 --- a/frontend/src/api/certificates.ts +++ /dev/null @@ -1,53 +0,0 @@ -import client from './client' - -/** Represents an SSL/TLS certificate. */ -export interface Certificate { - id?: number - name?: string - domain: string - issuer: string - expires_at: string - status: 'valid' | 'expiring' | 'expired' | 'untrusted' - provider: string -} - -/** - * Fetches all SSL certificates. - * @returns Promise resolving to array of Certificate objects - * @throws {AxiosError} If the request fails - */ -export async function getCertificates(): Promise { - const response = await client.get('/certificates') - return response.data -} - -/** - * Uploads a new SSL certificate with its private key. - * @param name - Display name for the certificate - * @param certFile - The certificate file (PEM format) - * @param keyFile - The private key file (PEM format) - * @returns Promise resolving to the created Certificate - * @throws {AxiosError} If upload fails or certificate is invalid - */ -export async function uploadCertificate(name: string, certFile: File, keyFile: File): Promise { - const formData = new FormData() - formData.append('name', name) - formData.append('certificate_file', certFile) - formData.append('key_file', keyFile) - - const response = await client.post('/certificates', formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }) - return response.data -} - -/** - * Deletes an SSL certificate. - * @param id - The ID of the certificate to delete - * @throws {AxiosError} If deletion fails or certificate not found - */ -export async function deleteCertificate(id: number): Promise { - await client.delete(`/certificates/${id}`) -} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts deleted file mode 100644 index c2657bdf..00000000 --- a/frontend/src/api/client.ts +++ /dev/null @@ -1,36 +0,0 @@ -import axios from 'axios'; - -/** - * Pre-configured Axios instance for API communication. - * Includes base URL, credentials, and timeout settings. - */ -const client = axios.create({ - baseURL: '/api/v1', - withCredentials: true, // Required for HttpOnly cookie transmission - timeout: 30000, // 30 second timeout -}); - -/** - * Sets or clears the Authorization header for API requests. - * @param token - JWT token to set, or null to clear authentication - */ -export const setAuthToken = (token: string | null) => { - if (token) { - client.defaults.headers.common.Authorization = `Bearer ${token}`; - } else { - delete client.defaults.headers.common.Authorization; - } -}; - -// Global 401 error logging for debugging -client.interceptors.response.use( - (response) => response, - (error) => { - if (error.response?.status === 401) { - console.warn('Authentication failed:', error.config?.url); - } - return Promise.reject(error); - } -); - -export default client; diff --git a/frontend/src/api/consoleEnrollment.ts b/frontend/src/api/consoleEnrollment.ts deleted file mode 100644 index 75c5e024..00000000 --- a/frontend/src/api/consoleEnrollment.ts +++ /dev/null @@ -1,57 +0,0 @@ -import client from './client' - -/** CrowdSec Console enrollment status. */ -export interface ConsoleEnrollmentStatus { - status: string - tenant?: string - agent_name?: string - last_error?: string - last_attempt_at?: string - enrolled_at?: string - last_heartbeat_at?: string - key_present: boolean - correlation_id?: string -} - -/** Payload for enrolling with CrowdSec Console. */ -export interface ConsoleEnrollPayload { - enrollment_key: string - tenant?: string - agent_name: string - force?: boolean -} - -/** - * Gets the current CrowdSec Console enrollment status. - * @returns Promise resolving to ConsoleEnrollmentStatus - * @throws {AxiosError} If status check fails - */ -export async function getConsoleStatus(): Promise { - const resp = await client.get('/admin/crowdsec/console/status') - return resp.data -} - -/** - * Enrolls the instance with CrowdSec Console. - * @param payload - Enrollment configuration including key and agent name - * @returns Promise resolving to the new enrollment status - * @throws {AxiosError} If enrollment fails - */ -export async function enrollConsole(payload: ConsoleEnrollPayload): Promise { - const resp = await client.post('/admin/crowdsec/console/enroll', payload) - return resp.data -} - -/** - * Clears the current CrowdSec Console enrollment. - * @throws {AxiosError} If clearing enrollment fails - */ -export async function clearConsoleEnrollment(): Promise { - await client.delete('/admin/crowdsec/console/enrollment') -} - -export default { - getConsoleStatus, - enrollConsole, - clearConsoleEnrollment, -} diff --git a/frontend/src/api/crowdsec.ts b/frontend/src/api/crowdsec.ts deleted file mode 100644 index fbe6df18..00000000 --- a/frontend/src/api/crowdsec.ts +++ /dev/null @@ -1,138 +0,0 @@ -import client from './client' - -/** Represents a CrowdSec decision (ban/captcha). */ -export interface CrowdSecDecision { - id: string - ip: string - reason: string - duration: string - created_at: string - source: string -} - -/** - * Starts the CrowdSec security service. - * @returns Promise resolving to status with process ID and LAPI readiness - * @throws {AxiosError} If the service fails to start - */ -export async function startCrowdsec(): Promise<{ status: string; pid: number; lapi_ready?: boolean }> { - const resp = await client.post('/admin/crowdsec/start') - return resp.data -} - -/** - * Stops the CrowdSec security service. - * @returns Promise resolving to stop status - * @throws {AxiosError} If the service fails to stop - */ -export async function stopCrowdsec() { - const resp = await client.post('/admin/crowdsec/stop') - return resp.data -} - -/** CrowdSec service status information. */ -export interface CrowdSecStatus { - running: boolean - pid: number - lapi_ready: boolean -} - -/** - * Gets the current status of the CrowdSec service. - * @returns Promise resolving to CrowdSecStatus - * @throws {AxiosError} If status check fails - */ -export async function statusCrowdsec(): Promise { - const resp = await client.get('/admin/crowdsec/status') - return resp.data -} - -/** - * Imports a CrowdSec configuration file. - * @param file - The configuration file to import - * @returns Promise resolving to import result - * @throws {AxiosError} If import fails or file is invalid - */ -export async function importCrowdsecConfig(file: File) { - const fd = new FormData() - fd.append('file', file) - const resp = await client.post('/admin/crowdsec/import', fd, { - headers: { 'Content-Type': 'multipart/form-data' }, - }) - return resp.data -} - -/** - * Exports the current CrowdSec configuration. - * @returns Promise resolving to configuration blob for download - * @throws {AxiosError} If export fails - */ -export async function exportCrowdsecConfig() { - const resp = await client.get('/admin/crowdsec/export', { responseType: 'blob' }) - return resp.data -} - -/** - * Lists all CrowdSec configuration files. - * @returns Promise resolving to object containing file list - * @throws {AxiosError} If listing fails - */ -export async function listCrowdsecFiles() { - const resp = await client.get<{ files: string[] }>('/admin/crowdsec/files') - return resp.data -} - -/** - * Reads the content of a CrowdSec configuration file. - * @param path - The file path to read - * @returns Promise resolving to object containing file content - * @throws {AxiosError} If file cannot be read - */ -export async function readCrowdsecFile(path: string) { - const resp = await client.get<{ content: string }>(`/admin/crowdsec/file?path=${encodeURIComponent(path)}`) - return resp.data -} - -/** - * Writes content to a CrowdSec configuration file. - * @param path - The file path to write - * @param content - The content to write - * @returns Promise resolving to write result - * @throws {AxiosError} If file cannot be written - */ -export async function writeCrowdsecFile(path: string, content: string) { - const resp = await client.post('/admin/crowdsec/file', { path, content }) - return resp.data -} - -/** - * Lists all active CrowdSec decisions (bans). - * @returns Promise resolving to object containing decisions array - * @throws {AxiosError} If listing fails - */ -export async function listCrowdsecDecisions(): Promise<{ decisions: CrowdSecDecision[] }> { - const resp = await client.get<{ decisions: CrowdSecDecision[] }>('/admin/crowdsec/decisions') - return resp.data -} - -/** - * Bans an IP address via CrowdSec. - * @param ip - The IP address to ban - * @param duration - Ban duration (e.g., "24h", "7d") - * @param reason - Reason for the ban - * @throws {AxiosError} If ban fails - */ -export async function banIP(ip: string, duration: string, reason: string): Promise { - await client.post('/admin/crowdsec/ban', { ip, duration, reason }) -} - -/** - * Removes a ban for an IP address. - * @param ip - The IP address to unban - * @throws {AxiosError} If unban fails - */ -export async function unbanIP(ip: string): Promise { - await client.delete(`/admin/crowdsec/ban/${encodeURIComponent(ip)}`) -} - -export default { startCrowdsec, stopCrowdsec, statusCrowdsec, importCrowdsecConfig, exportCrowdsecConfig, listCrowdsecFiles, readCrowdsecFile, writeCrowdsecFile, listCrowdsecDecisions, banIP, unbanIP } diff --git a/frontend/src/api/dnsProviders.ts b/frontend/src/api/dnsProviders.ts deleted file mode 100644 index dc3b514d..00000000 --- a/frontend/src/api/dnsProviders.ts +++ /dev/null @@ -1,163 +0,0 @@ -import client from './client' - -/** Supported DNS provider types */ -export type DNSProviderType = - | 'cloudflare' - | 'route53' - | 'digitalocean' - | 'googleclouddns' - | 'namecheap' - | 'godaddy' - | 'azure' - | 'hetzner' - | 'vultr' - | 'dnsimple' - -/** Represents a configured DNS provider */ -export interface DNSProvider { - id: number - uuid: string - name: string - provider_type: DNSProviderType - enabled: boolean - is_default: boolean - has_credentials: boolean - propagation_timeout: number - polling_interval: number - last_used_at?: string - success_count: number - failure_count: number - last_error?: string - created_at: string - updated_at: string -} - -/** Request payload for creating/updating DNS providers */ -export interface DNSProviderRequest { - name: string - provider_type: DNSProviderType - credentials: Record - propagation_timeout?: number - polling_interval?: number - is_default?: boolean -} - -/** DNS provider test result */ -export interface DNSTestResult { - success: boolean - message?: string - error?: string - code?: string - propagation_time_ms?: number -} - -/** DNS provider type information with field definitions */ -export interface DNSProviderTypeInfo { - type: DNSProviderType - name: string - fields: Array<{ - name: string - label: string - type: 'text' | 'password' - required: boolean - default?: string - hint?: string - }> - documentation_url: string -} - -/** Response for list endpoint */ -interface ListDNSProvidersResponse { - providers: DNSProvider[] - total: number -} - -/** Response for types endpoint */ -interface DNSProviderTypesResponse { - types: DNSProviderTypeInfo[] -} - -/** - * Fetches all configured DNS providers. - * @returns Promise resolving to array of DNS providers - * @throws {AxiosError} If the request fails - */ -export async function getDNSProviders(): Promise { - const response = await client.get('/dns-providers') - return response.data.providers -} - -/** - * Fetches a single DNS provider by ID. - * @param id - The DNS provider ID - * @returns Promise resolving to the DNS provider - * @throws {AxiosError} If not found or request fails - */ -export async function getDNSProvider(id: number): Promise { - const response = await client.get(`/dns-providers/${id}`) - return response.data -} - -/** - * Creates a new DNS provider. - * @param data - DNS provider configuration - * @returns Promise resolving to the created provider - * @throws {AxiosError} If validation fails or request fails - */ -export async function createDNSProvider(data: DNSProviderRequest): Promise { - const response = await client.post('/dns-providers', data) - return response.data -} - -/** - * Updates an existing DNS provider. - * @param id - The DNS provider ID - * @param data - Updated configuration - * @returns Promise resolving to the updated provider - * @throws {AxiosError} If not found, validation fails, or request fails - */ -export async function updateDNSProvider(id: number, data: DNSProviderRequest): Promise { - const response = await client.put(`/dns-providers/${id}`, data) - return response.data -} - -/** - * Deletes a DNS provider. - * @param id - The DNS provider ID - * @throws {AxiosError} If not found or in use by proxy hosts - */ -export async function deleteDNSProvider(id: number): Promise { - await client.delete(`/dns-providers/${id}`) -} - -/** - * Tests connectivity of a saved DNS provider. - * @param id - The DNS provider ID - * @returns Promise resolving to test result - * @throws {AxiosError} If not found or request fails - */ -export async function testDNSProvider(id: number): Promise { - const response = await client.post(`/dns-providers/${id}/test`) - return response.data -} - -/** - * Tests DNS provider credentials before saving. - * @param data - Provider configuration to test - * @returns Promise resolving to test result - * @throws {AxiosError} If validation fails or request fails - */ -export async function testDNSProviderCredentials(data: DNSProviderRequest): Promise { - const response = await client.post('/dns-providers/test', data) - return response.data -} - -/** - * Fetches supported DNS provider types with field definitions. - * @returns Promise resolving to array of provider type info - * @throws {AxiosError} If request fails - */ -export async function getDNSProviderTypes(): Promise { - const response = await client.get('/dns-providers/types') - return response.data.types -} diff --git a/frontend/src/api/docker.ts b/frontend/src/api/docker.ts deleted file mode 100644 index 47013643..00000000 --- a/frontend/src/api/docker.ts +++ /dev/null @@ -1,39 +0,0 @@ -import client from './client' - -/** Docker port mapping information. */ -export interface DockerPort { - private_port: number - public_port: number - type: string -} - -/** Docker container information. */ -export interface DockerContainer { - id: string - names: string[] - image: string - state: string - status: string - network: string - ip: string - ports: DockerPort[] -} - -/** Docker API client for container operations. */ -export const dockerApi = { - /** - * Lists Docker containers from a local or remote host. - * @param host - Optional Docker host address - * @param serverId - Optional remote server ID - * @returns Promise resolving to array of DockerContainer objects - * @throws {AxiosError} If listing fails or host unreachable - */ - listContainers: async (host?: string, serverId?: string): Promise => { - const params: Record = {} - if (host) params.host = host - if (serverId) params.server_id = serverId - - const response = await client.get('/docker/containers', { params }) - return response.data - }, -} diff --git a/frontend/src/api/domains.ts b/frontend/src/api/domains.ts deleted file mode 100644 index 4ce61586..00000000 --- a/frontend/src/api/domains.ts +++ /dev/null @@ -1,39 +0,0 @@ -import client from './client' - -/** Represents a managed domain. */ -export interface Domain { - id: number - uuid: string - name: string - created_at: string -} - -/** - * Fetches all managed domains. - * @returns Promise resolving to array of Domain objects - * @throws {AxiosError} If the request fails - */ -export const getDomains = async (): Promise => { - const { data } = await client.get('/domains') - return data -} - -/** - * Creates a new managed domain. - * @param name - The domain name to create - * @returns Promise resolving to the created Domain - * @throws {AxiosError} If creation fails or domain is invalid - */ -export const createDomain = async (name: string): Promise => { - const { data } = await client.post('/domains', { name }) - return data -} - -/** - * Deletes a managed domain. - * @param uuid - The unique identifier of the domain to delete - * @throws {AxiosError} If deletion fails or domain not found - */ -export const deleteDomain = async (uuid: string): Promise => { - await client.delete(`/domains/${uuid}`) -} diff --git a/frontend/src/api/featureFlags.test.ts b/frontend/src/api/featureFlags.test.ts deleted file mode 100644 index 66d3ec25..00000000 --- a/frontend/src/api/featureFlags.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { vi, describe, it, expect } from 'vitest' - -// Mock the client module which is an axios instance wrapper -vi.mock('./client', () => ({ - default: { - get: vi.fn(() => Promise.resolve({ data: { 'feature.cerberus.enabled': true } })), - put: vi.fn(() => Promise.resolve({ data: { status: 'ok' } })), - }, -})) - -import { getFeatureFlags, updateFeatureFlags } from './featureFlags' -import client from './client' - -describe('featureFlags API', () => { - it('fetches feature flags', async () => { - const flags = await getFeatureFlags() - expect(flags['feature.cerberus.enabled']).toBe(true) - expect(vi.mocked(client.get)).toHaveBeenCalled() - }) - - it('updates feature flags', async () => { - const resp = await updateFeatureFlags({ 'feature.cerberus.enabled': false }) - expect(resp).toEqual({ status: 'ok' }) - expect(vi.mocked(client.put)).toHaveBeenCalledWith('/feature-flags', { 'feature.cerberus.enabled': false }) - }) -}) diff --git a/frontend/src/api/featureFlags.ts b/frontend/src/api/featureFlags.ts deleted file mode 100644 index dd0e9f26..00000000 --- a/frontend/src/api/featureFlags.ts +++ /dev/null @@ -1,27 +0,0 @@ -import client from './client' - -/** - * Fetches all feature flags and their current states. - * @returns Promise resolving to a record of flag names to boolean values - * @throws {AxiosError} If the request fails - */ -export async function getFeatureFlags(): Promise> { - const resp = await client.get>('/feature-flags') - return resp.data -} - -/** - * Updates one or more feature flags. - * @param payload - Record of flag names to new boolean values - * @returns Promise resolving to the update result - * @throws {AxiosError} If the update fails - */ -export async function updateFeatureFlags(payload: Record) { - const resp = await client.put('/feature-flags', payload) - return resp.data -} - -export default { - getFeatureFlags, - updateFeatureFlags, -} diff --git a/frontend/src/api/health.ts b/frontend/src/api/health.ts deleted file mode 100644 index e402c79c..00000000 --- a/frontend/src/api/health.ts +++ /dev/null @@ -1,20 +0,0 @@ -import client from './client'; - -/** Health check response with version and build information. */ -export interface HealthResponse { - status: string; - service: string; - version: string; - git_commit: string; - build_time: string; -} - -/** - * Checks the health status of the API server. - * @returns Promise resolving to HealthResponse with version info - * @throws {AxiosError} If the health check fails - */ -export const checkHealth = async (): Promise => { - const { data } = await client.get('/health'); - return data; -}; diff --git a/frontend/src/api/import.ts b/frontend/src/api/import.ts deleted file mode 100644 index 8519c0e2..00000000 --- a/frontend/src/api/import.ts +++ /dev/null @@ -1,127 +0,0 @@ -import client from './client'; - -/** Represents an active import session. */ -export interface ImportSession { - id: string; - state: 'pending' | 'reviewing' | 'completed' | 'failed' | 'transient'; - created_at: string; - updated_at: string; - source_file?: string; -} - -/** Preview of a Caddyfile import with hosts and conflicts. */ -export interface ImportPreview { - session: ImportSession; - preview: { - hosts: Array<{ domain_names: string; [key: string]: unknown }>; - conflicts: string[]; - errors: string[]; - }; - caddyfile_content?: string; - conflict_details?: Record; -} - -/** - * Uploads a Caddyfile content for import preview. - * @param content - The Caddyfile content as a string - * @returns Promise resolving to ImportPreview with parsed hosts - * @throws {AxiosError} If parsing fails or content is invalid - */ -export const uploadCaddyfile = async (content: string): Promise => { - const { data } = await client.post('/import/upload', { content }); - return data; -}; - -/** - * Uploads multiple Caddyfile contents for batch import. - * @param contents - Array of Caddyfile content strings - * @returns Promise resolving to combined ImportPreview - * @throws {AxiosError} If parsing fails - */ -export const uploadCaddyfilesMulti = async (contents: string[]): Promise => { - const { data } = await client.post('/import/upload-multi', { contents }); - return data; -}; - -/** - * Gets the current import preview for the active session. - * @returns Promise resolving to ImportPreview - * @throws {AxiosError} If no active session or request fails - */ -export const getImportPreview = async (): Promise => { - const { data } = await client.get('/import/preview'); - return data; -}; - -/** Result of committing an import operation. */ -export interface ImportCommitResult { - created: number; - updated: number; - skipped: number; - errors: string[]; -} - -/** - * Commits the import, creating/updating proxy hosts. - * @param sessionUUID - The import session UUID - * @param resolutions - Map of conflict resolutions (domain -> 'keep'|'replace'|'skip') - * @param names - Map of custom names for imported hosts - * @returns Promise resolving to ImportCommitResult with counts - * @throws {AxiosError} If commit fails - */ -export const commitImport = async ( - sessionUUID: string, - resolutions: Record, - names: Record -): Promise => { - const { data } = await client.post('/import/commit', { - session_uuid: sessionUUID, - resolutions, - names, - }); - return data; -}; - -/** - * Cancels the current import session. - * @throws {AxiosError} If cancellation fails - */ -export const cancelImport = async (): Promise => { - await client.post('/import/cancel'); -}; - -/** - * Gets the current import session status. - * @returns Promise resolving to object with pending status and optional session - */ -export const getImportStatus = async (): Promise<{ has_pending: boolean; session?: ImportSession }> => { - // Note: Assuming there might be a status endpoint or we infer from preview. - // If no dedicated status endpoint exists in backend, we might rely on preview returning 404 or empty. - // Based on previous context, there wasn't an explicit status endpoint mentioned in the simple API, - // but the hook used `importAPI.status()`. I'll check the backend routes if needed. - // For now, I'll implement it assuming /import/preview can serve as status check or there is a /import/status. - // Let's check the backend routes to be sure. - try { - const { data } = await client.get<{ has_pending: boolean; session?: ImportSession }>('/import/status'); - return data; - } catch { - // Fallback if status endpoint doesn't exist, though the hook used it. - return { has_pending: false }; - } -}; diff --git a/frontend/src/api/logs.test.ts b/frontend/src/api/logs.test.ts deleted file mode 100644 index 02c03c42..00000000 --- a/frontend/src/api/logs.test.ts +++ /dev/null @@ -1,339 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' -import client from './client' -import { getLogs, getLogContent, downloadLog, connectLiveLogs, connectSecurityLogs } from './logs' -import type { LiveLogEntry, SecurityLogEntry } from './logs' - -vi.mock('./client', () => ({ - default: { - get: vi.fn(), - }, -})) - -const mockedClient = client as unknown as { - get: ReturnType -} - -class MockWebSocket { - static CONNECTING = 0 - static OPEN = 1 - static CLOSED = 3 - static instances: MockWebSocket[] = [] - - url: string - readyState = MockWebSocket.CONNECTING - onopen: (() => void) | null = null - onmessage: ((event: { data: string }) => void) | null = null - onerror: ((event: Event) => void) | null = null - onclose: ((event: CloseEvent) => void) | null = null - - constructor(url: string) { - this.url = url - MockWebSocket.instances.push(this) - } - - open() { - this.readyState = MockWebSocket.OPEN - this.onopen?.() - } - - sendMessage(data: string) { - this.onmessage?.({ data }) - } - - triggerError(event: Event) { - this.onerror?.(event) - } - - close() { - this.readyState = MockWebSocket.CLOSED - this.onclose?.({ code: 1000, reason: '', wasClean: true } as CloseEvent) - } -} - -const originalWebSocket = globalThis.WebSocket -const originalLocation = { ...window.location } - -beforeEach(() => { - vi.clearAllMocks() - ;(globalThis as unknown as { WebSocket: typeof WebSocket }).WebSocket = MockWebSocket as unknown as typeof WebSocket - Object.defineProperty(window, 'location', { - value: { ...originalLocation, protocol: 'http:', host: 'localhost', href: '' }, - writable: true, - }) -}) - -afterEach(() => { - ;(globalThis as unknown as { WebSocket: typeof WebSocket }).WebSocket = originalWebSocket - Object.defineProperty(window, 'location', { value: originalLocation }) - MockWebSocket.instances.length = 0 -}) - -describe('logs api', () => { - it('lists log files', async () => { - mockedClient.get.mockResolvedValue({ data: [{ name: 'access.log', size: 10, mod_time: 'now' }] }) - - const logs = await getLogs() - - expect(mockedClient.get).toHaveBeenCalledWith('/logs') - expect(logs[0].name).toBe('access.log') - }) - - it('fetches log content with filters applied', async () => { - mockedClient.get.mockResolvedValue({ data: { filename: 'access.log', logs: [], total: 0, limit: 50, offset: 0 } }) - - await getLogContent('access.log', { - search: 'error', - host: 'example.com', - status: '500', - level: 'error', - limit: 50, - offset: 10, - sort: 'asc', - }) - - expect(mockedClient.get).toHaveBeenCalledWith( - '/logs/access.log?search=error&host=example.com&status=500&level=error&limit=50&offset=10&sort=asc' - ) - }) - - it('sets window location when downloading logs', () => { - downloadLog('access.log') - expect(window.location.href).toBe('/api/v1/logs/access.log/download') - }) - - it('connects to live logs websocket and handles lifecycle events', () => { - const received: LiveLogEntry[] = [] - const onOpen = vi.fn() - const onError = vi.fn() - const onClose = vi.fn() - - const disconnect = connectLiveLogs({ level: 'error', source: 'cerberus' }, (log) => received.push(log), onOpen, onError, onClose) - - const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]! - expect(socket.url).toContain('level=error') - expect(socket.url).toContain('source=cerberus') - - socket.open() - expect(onOpen).toHaveBeenCalled() - - socket.sendMessage(JSON.stringify({ level: 'info', timestamp: 'now', message: 'hello' })) - expect(received).toHaveLength(1) - - const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) - socket.sendMessage('not-json') - expect(consoleError).toHaveBeenCalled() - consoleError.mockRestore() - - const errorEvent = new Event('error') - socket.triggerError(errorEvent) - expect(onError).toHaveBeenCalledWith(errorEvent) - - socket.close() - expect(onClose).toHaveBeenCalled() - - disconnect() - }) -}) - -describe('connectSecurityLogs', () => { - it('connects to cerberus logs websocket endpoint', () => { - const received: SecurityLogEntry[] = [] - const onOpen = vi.fn() - - connectSecurityLogs({}, (log) => received.push(log), onOpen) - - const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]! - expect(socket.url).toContain('/api/v1/cerberus/logs/ws') - }) - - it('passes source filter to websocket url', () => { - connectSecurityLogs({ source: 'waf' }, () => {}) - - const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]! - expect(socket.url).toContain('source=waf') - }) - - it('passes level filter to websocket url', () => { - connectSecurityLogs({ level: 'error' }, () => {}) - - const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]! - expect(socket.url).toContain('level=error') - }) - - it('passes ip filter to websocket url', () => { - connectSecurityLogs({ ip: '192.168' }, () => {}) - - const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]! - expect(socket.url).toContain('ip=192.168') - }) - - it('passes host filter to websocket url', () => { - connectSecurityLogs({ host: 'example.com' }, () => {}) - - const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]! - expect(socket.url).toContain('host=example.com') - }) - - it('passes blocked_only filter to websocket url', () => { - connectSecurityLogs({ blocked_only: true }, () => {}) - - const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]! - expect(socket.url).toContain('blocked_only=true') - }) - - it('receives and parses security log entries', () => { - const received: SecurityLogEntry[] = [] - connectSecurityLogs({}, (log) => received.push(log)) - - const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]! - socket.open() - - const securityLogEntry: SecurityLogEntry = { - timestamp: '2025-12-12T10:30:00Z', - level: 'info', - logger: 'http.log.access', - client_ip: '192.168.1.100', - method: 'GET', - uri: '/api/test', - status: 200, - duration: 0.05, - size: 1024, - user_agent: 'TestAgent/1.0', - host: 'example.com', - source: 'normal', - blocked: false, - } - - socket.sendMessage(JSON.stringify(securityLogEntry)) - - expect(received).toHaveLength(1) - expect(received[0].client_ip).toBe('192.168.1.100') - expect(received[0].source).toBe('normal') - expect(received[0].blocked).toBe(false) - }) - - it('receives blocked security log entries', () => { - const received: SecurityLogEntry[] = [] - connectSecurityLogs({}, (log) => received.push(log)) - - const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]! - socket.open() - - const blockedEntry: SecurityLogEntry = { - timestamp: '2025-12-12T10:30:00Z', - level: 'warn', - logger: 'http.handlers.waf', - client_ip: '10.0.0.1', - method: 'POST', - uri: '/admin', - status: 403, - duration: 0.001, - size: 0, - user_agent: 'Attack/1.0', - host: 'example.com', - source: 'waf', - blocked: true, - block_reason: 'SQL injection detected', - } - - socket.sendMessage(JSON.stringify(blockedEntry)) - - expect(received).toHaveLength(1) - expect(received[0].blocked).toBe(true) - expect(received[0].block_reason).toBe('SQL injection detected') - expect(received[0].source).toBe('waf') - }) - - it('handles onOpen callback', () => { - const onOpen = vi.fn() - connectSecurityLogs({}, () => {}, onOpen) - - const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]! - socket.open() - - expect(onOpen).toHaveBeenCalled() - }) - - it('handles onError callback', () => { - const onError = vi.fn() - connectSecurityLogs({}, () => {}, undefined, onError) - - const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]! - const errorEvent = new Event('error') - socket.triggerError(errorEvent) - - expect(onError).toHaveBeenCalledWith(errorEvent) - }) - - it('handles onClose callback', () => { - const onClose = vi.fn() - connectSecurityLogs({}, () => {}, undefined, undefined, onClose) - - const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]! - socket.close() - - expect(onClose).toHaveBeenCalled() - }) - - it('returns disconnect function that closes websocket', () => { - const disconnect = connectSecurityLogs({}, () => {}) - - const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]! - socket.open() - - expect(socket.readyState).toBe(MockWebSocket.OPEN) - - disconnect() - - expect(socket.readyState).toBe(MockWebSocket.CLOSED) - }) - - it('handles JSON parse errors gracefully', () => { - const received: SecurityLogEntry[] = [] - const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) - - connectSecurityLogs({}, (log) => received.push(log)) - - const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]! - socket.open() - socket.sendMessage('invalid-json') - - expect(received).toHaveLength(0) - expect(consoleError).toHaveBeenCalled() - - consoleError.mockRestore() - }) - - it('uses wss protocol when on https', () => { - Object.defineProperty(window, 'location', { - value: { protocol: 'https:', host: 'secure.example.com', href: '' }, - writable: true, - }) - - connectSecurityLogs({}, () => {}) - - const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]! - expect(socket.url).toContain('wss://') - expect(socket.url).toContain('secure.example.com') - }) - - it('combines multiple filters in websocket url', () => { - connectSecurityLogs( - { - source: 'waf', - level: 'warn', - ip: '10.0.0', - host: 'example.com', - blocked_only: true, - }, - () => {} - ) - - const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]! - expect(socket.url).toContain('source=waf') - expect(socket.url).toContain('level=warn') - expect(socket.url).toContain('ip=10.0.0') - expect(socket.url).toContain('host=example.com') - expect(socket.url).toContain('blocked_only=true') - }) -}) diff --git a/frontend/src/api/logs.ts b/frontend/src/api/logs.ts deleted file mode 100644 index 2812c682..00000000 --- a/frontend/src/api/logs.ts +++ /dev/null @@ -1,262 +0,0 @@ -import client from './client'; - -/** Represents a log file on the server. */ -export interface LogFile { - name: string; - size: number; - mod_time: string; -} - -/** Parsed Caddy access log entry. */ -export interface CaddyAccessLog { - level: string; - ts: number; - logger: string; - msg: string; - request: { - remote_ip: string; - method: string; - host: string; - uri: string; - proto: string; - }; - status: number; - duration: number; - size: number; -} - -/** Paginated log response. */ -export interface LogResponse { - filename: string; - logs: CaddyAccessLog[]; - total: number; - limit: number; - offset: number; -} - -/** Filter options for log queries. */ -export interface LogFilter { - search?: string; - host?: string; - status?: string; - level?: string; - limit?: number; - offset?: number; - sort?: 'asc' | 'desc'; -} - -/** - * Fetches the list of available log files. - * @returns Promise resolving to array of LogFile objects - * @throws {AxiosError} If the request fails - */ -export const getLogs = async (): Promise => { - const response = await client.get('/logs'); - return response.data; -}; - -/** - * Fetches paginated and filtered log entries from a specific file. - * @param filename - The log file name to read - * @param filter - Optional filter and pagination options - * @returns Promise resolving to LogResponse with entries and metadata - * @throws {AxiosError} If the request fails or file not found - */ -export const getLogContent = async (filename: string, filter: LogFilter = {}): Promise => { - const params = new URLSearchParams(); - if (filter.search) params.append('search', filter.search); - if (filter.host) params.append('host', filter.host); - if (filter.status) params.append('status', filter.status); - if (filter.level) params.append('level', filter.level); - if (filter.limit) params.append('limit', filter.limit.toString()); - if (filter.offset) params.append('offset', filter.offset.toString()); - if (filter.sort) params.append('sort', filter.sort); - - const response = await client.get(`/logs/${filename}?${params.toString()}`); - return response.data; -}; - -/** - * Initiates a log file download by redirecting the browser. - * @param filename - The log file name to download - */ -export const downloadLog = (filename: string) => { - // Direct window location change to trigger download - // We need to use the base URL from the client config if possible, - // but for now we assume relative path works with the proxy setup - window.location.href = `/api/v1/logs/${filename}/download`; -}; - -/** Live log entry from WebSocket stream. */ -export interface LiveLogEntry { - level: string; - timestamp: string; - message: string; - source?: string; - data?: Record; -} - -/** Filter options for live log streaming. */ -export interface LiveLogFilter { - level?: string; - source?: string; -} - -/** - * SecurityLogEntry represents a security-relevant log entry from Cerberus. - * This matches the backend SecurityLogEntry struct from /api/v1/cerberus/logs/ws - */ -export interface SecurityLogEntry { - timestamp: string; - level: string; - logger: string; - client_ip: string; - method: string; - uri: string; - status: number; - duration: number; - size: number; - user_agent: string; - host: string; - source: 'waf' | 'crowdsec' | 'ratelimit' | 'acl' | 'normal'; - blocked: boolean; - block_reason?: string; - details?: Record; -} - -/** - * Filters for the Cerberus security logs WebSocket endpoint. - */ -export interface SecurityLogFilter { - source?: string; // Filter by security module: waf, crowdsec, ratelimit, acl, normal - level?: string; // Filter by log level: info, warn, error - ip?: string; // Filter by client IP (partial match) - host?: string; // Filter by host (partial match) - blocked_only?: boolean; // Only show blocked requests -} - -/** - * Connects to the live logs WebSocket endpoint for real-time log streaming. - * Returns a cleanup function to close the connection. - * @param filters - LiveLogFilter options for level and source filtering - * @param onMessage - Callback invoked for each received LiveLogEntry - * @param onOpen - Optional callback when WebSocket connection is established - * @param onError - Optional callback on WebSocket error - * @param onClose - Optional callback when WebSocket connection closes - * @returns Function to close the WebSocket connection - */ -export const connectLiveLogs = ( - filters: LiveLogFilter, - onMessage: (log: LiveLogEntry) => void, - onOpen?: () => void, - onError?: (error: Event) => void, - onClose?: () => void -): (() => void) => { - const params = new URLSearchParams(); - if (filters.level) params.append('level', filters.level); - if (filters.source) params.append('source', filters.source); - - // Authentication is handled via HttpOnly cookies sent automatically by the browser - // This prevents tokens from being logged in access logs or exposed to XSS attacks - - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${protocol}//${window.location.host}/api/v1/logs/live?${params.toString()}`; - - console.log('Connecting to WebSocket:', wsUrl); - const ws = new WebSocket(wsUrl); - - ws.onopen = () => { - console.log('WebSocket connection established'); - onOpen?.(); - }; - - ws.onmessage = (event: MessageEvent) => { - try { - const log = JSON.parse(event.data) as LiveLogEntry; - onMessage(log); - } catch (err) { - console.error('Failed to parse log message:', err); - } - }; - - ws.onerror = (error: Event) => { - console.error('WebSocket error:', error); - onError?.(error); - }; - - ws.onclose = (event: CloseEvent) => { - console.log('WebSocket connection closed', { code: event.code, reason: event.reason, wasClean: event.wasClean }); - onClose?.(); - }; - - return () => { - if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) { - ws.close(); - } - }; -}; - -/** - * Connects to the Cerberus security logs WebSocket endpoint. - * This streams parsed Caddy access logs with security event annotations. - * - * @param filters - Optional filters for source, level, IP, host, and blocked_only - * @param onMessage - Callback for each received SecurityLogEntry - * @param onOpen - Callback when connection is established - * @param onError - Callback on connection error - * @param onClose - Callback when connection closes - * @returns A function to close the WebSocket connection - */ -export const connectSecurityLogs = ( - filters: SecurityLogFilter, - onMessage: (log: SecurityLogEntry) => void, - onOpen?: () => void, - onError?: (error: Event) => void, - onClose?: () => void -): (() => void) => { - const params = new URLSearchParams(); - if (filters.source) params.append('source', filters.source); - if (filters.level) params.append('level', filters.level); - if (filters.ip) params.append('ip', filters.ip); - if (filters.host) params.append('host', filters.host); - if (filters.blocked_only) params.append('blocked_only', 'true'); - - // Authentication is handled via HttpOnly cookies sent automatically by the browser - // This prevents tokens from being logged in access logs or exposed to XSS attacks - - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${protocol}//${window.location.host}/api/v1/cerberus/logs/ws?${params.toString()}`; - - console.log('Connecting to Cerberus logs WebSocket:', wsUrl); - const ws = new WebSocket(wsUrl); - - ws.onopen = () => { - console.log('Cerberus logs WebSocket connection established'); - onOpen?.(); - }; - - ws.onmessage = (event: MessageEvent) => { - try { - const log = JSON.parse(event.data) as SecurityLogEntry; - onMessage(log); - } catch (err) { - console.error('Failed to parse security log message:', err); - } - }; - - ws.onerror = (error: Event) => { - console.error('Cerberus logs WebSocket error:', error); - onError?.(error); - }; - - ws.onclose = (event: CloseEvent) => { - console.log('Cerberus logs WebSocket closed', { code: event.code, reason: event.reason, wasClean: event.wasClean }); - onClose?.(); - }; - - return () => { - if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) { - ws.close(); - } - }; -}; diff --git a/frontend/src/api/notifications.test.ts b/frontend/src/api/notifications.test.ts deleted file mode 100644 index efabb1bc..00000000 --- a/frontend/src/api/notifications.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' -import client from './client' -import { - getProviders, - createProvider, - updateProvider, - deleteProvider, - testProvider, - getTemplates, - previewProvider, - getExternalTemplates, - createExternalTemplate, - updateExternalTemplate, - deleteExternalTemplate, - previewExternalTemplate, - getSecurityNotificationSettings, - updateSecurityNotificationSettings, -} from './notifications' - -vi.mock('./client', () => ({ - default: { - get: vi.fn(), - post: vi.fn(), - put: vi.fn(), - delete: vi.fn(), - }, -})) - -const mockedClient = client as unknown as { - get: ReturnType - post: ReturnType - put: ReturnType - delete: ReturnType -} - -describe('notifications api', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it('fetches providers list', async () => { - mockedClient.get.mockResolvedValue({ - data: [ - { - id: '1', - name: 'PagerDuty', - type: 'webhook', - url: 'https://hooks.example.com', - enabled: true, - notify_proxy_hosts: true, - notify_remote_servers: false, - notify_domains: false, - notify_certs: false, - notify_uptime: true, - created_at: '2025-01-01T00:00:00Z', - }, - ], - }) - - const result = await getProviders() - - expect(mockedClient.get).toHaveBeenCalledWith('/notifications/providers') - expect(result[0].name).toBe('PagerDuty') - }) - - it('creates, updates, tests, and deletes a provider', async () => { - mockedClient.post.mockResolvedValue({ data: { id: 'new', name: 'Slack' } }) - mockedClient.put.mockResolvedValue({ data: { id: 'new', name: 'Slack v2' } }) - - const created = await createProvider({ name: 'Slack' }) - expect(mockedClient.post).toHaveBeenCalledWith('/notifications/providers', { name: 'Slack' }) - expect(created.id).toBe('new') - - const updated = await updateProvider('new', { enabled: false }) - expect(mockedClient.put).toHaveBeenCalledWith('/notifications/providers/new', { enabled: false }) - expect(updated.name).toBe('Slack v2') - - await testProvider({ id: 'new', name: 'Slack', enabled: true }) - expect(mockedClient.post).toHaveBeenCalledWith('/notifications/providers/test', { - id: 'new', - name: 'Slack', - enabled: true, - }) - - mockedClient.delete.mockResolvedValue({}) - await deleteProvider('new') - expect(mockedClient.delete).toHaveBeenCalledWith('/notifications/providers/new') - }) - - it('fetches templates and previews provider payloads with data', async () => { - mockedClient.get.mockResolvedValueOnce({ data: [{ id: 'tpl', name: 'default' }] }) - mockedClient.post.mockResolvedValue({ data: { preview: 'ok' } }) - - const templates = await getTemplates() - expect(mockedClient.get).toHaveBeenCalledWith('/notifications/templates') - expect(templates[0].id).toBe('tpl') - - const preview = await previewProvider({ id: 'p1', name: 'Provider' }, { foo: 'bar' }) - expect(mockedClient.post).toHaveBeenCalledWith('/notifications/providers/preview', { - id: 'p1', - name: 'Provider', - data: { foo: 'bar' }, - }) - expect(preview).toEqual({ preview: 'ok' }) - }) - - it('handles external templates lifecycle and previews', async () => { - mockedClient.get.mockResolvedValueOnce({ data: [{ id: 'ext', name: 'External' }] }) - mockedClient.post.mockResolvedValueOnce({ data: { id: 'ext', name: 'created' } }) - mockedClient.put.mockResolvedValueOnce({ data: { id: 'ext', name: 'updated' } }) - mockedClient.post.mockResolvedValueOnce({ data: { preview: 'rendered' } }) - - const list = await getExternalTemplates() - expect(mockedClient.get).toHaveBeenCalledWith('/notifications/external-templates') - expect(list[0].id).toBe('ext') - - const created = await createExternalTemplate({ name: 'External' }) - expect(mockedClient.post).toHaveBeenCalledWith('/notifications/external-templates', { name: 'External' }) - expect(created.name).toBe('created') - - const updated = await updateExternalTemplate('ext', { description: 'desc' }) - expect(mockedClient.put).toHaveBeenCalledWith('/notifications/external-templates/ext', { description: 'desc' }) - expect(updated.name).toBe('updated') - - await deleteExternalTemplate('ext') - expect(mockedClient.delete).toHaveBeenCalledWith('/notifications/external-templates/ext') - - const preview = await previewExternalTemplate('ext', '', { a: 1 }) - expect(mockedClient.post).toHaveBeenCalledWith('/notifications/external-templates/preview', { - template_id: 'ext', - template: '', - data: { a: 1 }, - }) - expect(preview).toEqual({ preview: 'rendered' }) - }) - - it('reads and updates security notification settings', async () => { - mockedClient.get.mockResolvedValueOnce({ data: { enabled: true, min_log_level: 'info', notify_waf_blocks: true, notify_acl_denials: false, notify_rate_limit_hits: true } }) - mockedClient.put.mockResolvedValueOnce({ data: { enabled: false, min_log_level: 'error', notify_waf_blocks: false, notify_acl_denials: true, notify_rate_limit_hits: false } }) - - const settings = await getSecurityNotificationSettings() - expect(settings.enabled).toBe(true) - expect(mockedClient.get).toHaveBeenCalledWith('/notifications/settings/security') - - const updated = await updateSecurityNotificationSettings({ enabled: false, min_log_level: 'error' }) - expect(mockedClient.put).toHaveBeenCalledWith('/notifications/settings/security', { enabled: false, min_log_level: 'error' }) - expect(updated.enabled).toBe(false) - }) -}) diff --git a/frontend/src/api/notifications.ts b/frontend/src/api/notifications.ts deleted file mode 100644 index 1fce8865..00000000 --- a/frontend/src/api/notifications.ts +++ /dev/null @@ -1,204 +0,0 @@ -import client from './client'; - -/** Notification provider configuration. */ -export interface NotificationProvider { - id: string; - name: string; - type: string; - url: string; - config?: string; - template?: string; - enabled: boolean; - notify_proxy_hosts: boolean; - notify_remote_servers: boolean; - notify_domains: boolean; - notify_certs: boolean; - notify_uptime: boolean; - created_at: string; -} - -/** - * Fetches all notification providers. - * @returns Promise resolving to array of NotificationProvider objects - * @throws {AxiosError} If the request fails - */ -export const getProviders = async () => { - const response = await client.get('/notifications/providers'); - return response.data; -}; - -/** - * Creates a new notification provider. - * @param data - Partial NotificationProvider configuration - * @returns Promise resolving to the created NotificationProvider - * @throws {AxiosError} If creation fails - */ -export const createProvider = async (data: Partial) => { - const response = await client.post('/notifications/providers', data); - return response.data; -}; - -/** - * Updates an existing notification provider. - * @param id - The provider ID to update - * @param data - Partial NotificationProvider with fields to update - * @returns Promise resolving to the updated NotificationProvider - * @throws {AxiosError} If update fails or provider not found - */ -export const updateProvider = async (id: string, data: Partial) => { - const response = await client.put(`/notifications/providers/${id}`, data); - return response.data; -}; - -/** - * Deletes a notification provider. - * @param id - The provider ID to delete - * @throws {AxiosError} If deletion fails or provider not found - */ -export const deleteProvider = async (id: string) => { - await client.delete(`/notifications/providers/${id}`); -}; - -/** - * Tests a notification provider by sending a test message. - * @param provider - Provider configuration to test - * @throws {AxiosError} If test fails - */ -export const testProvider = async (provider: Partial) => { - await client.post('/notifications/providers/test', provider); -}; - -/** - * Fetches all available notification templates. - * @returns Promise resolving to array of NotificationTemplate objects - * @throws {AxiosError} If the request fails - */ -export const getTemplates = async () => { - const response = await client.get('/notifications/templates'); - return response.data; -}; - -/** Notification template definition. */ -export interface NotificationTemplate { - id: string; - name: string; -} - -/** - * Previews a notification with sample data. - * @param provider - Provider configuration for preview - * @param data - Optional sample data for template rendering - * @returns Promise resolving to preview result - * @throws {AxiosError} If preview fails - */ -export const previewProvider = async (provider: Partial, data?: Record) => { - const payload: Record = { ...provider } as Record; - if (data) payload.data = data; - const response = await client.post('/notifications/providers/preview', payload); - return response.data; -}; - -// External (saved) templates API -/** External notification template configuration. */ -export interface ExternalTemplate { - id: string; - name: string; - description?: string; - config?: string; - template?: string; - created_at?: string; -} - -/** - * Fetches all external notification templates. - * @returns Promise resolving to array of ExternalTemplate objects - * @throws {AxiosError} If the request fails - */ -export const getExternalTemplates = async () => { - const response = await client.get('/notifications/external-templates'); - return response.data; -}; - -/** - * Creates a new external notification template. - * @param data - Partial ExternalTemplate configuration - * @returns Promise resolving to the created ExternalTemplate - * @throws {AxiosError} If creation fails - */ -export const createExternalTemplate = async (data: Partial) => { - const response = await client.post('/notifications/external-templates', data); - return response.data; -}; - -/** - * Updates an existing external notification template. - * @param id - The template ID to update - * @param data - Partial ExternalTemplate with fields to update - * @returns Promise resolving to the updated ExternalTemplate - * @throws {AxiosError} If update fails or template not found - */ -export const updateExternalTemplate = async (id: string, data: Partial) => { - const response = await client.put(`/notifications/external-templates/${id}`, data); - return response.data; -}; - -/** - * Deletes an external notification template. - * @param id - The template ID to delete - * @throws {AxiosError} If deletion fails or template not found - */ -export const deleteExternalTemplate = async (id: string) => { - await client.delete(`/notifications/external-templates/${id}`); -}; - -/** - * Previews an external template with sample data. - * @param templateId - Optional existing template ID to preview - * @param template - Optional template content string - * @param data - Optional sample data for rendering - * @returns Promise resolving to preview result - * @throws {AxiosError} If preview fails - */ -export const previewExternalTemplate = async (templateId?: string, template?: string, data?: Record) => { - const payload: Record = {}; - if (templateId) payload.template_id = templateId; - if (template) payload.template = template; - if (data) payload.data = data; - const response = await client.post('/notifications/external-templates/preview', payload); - return response.data; -}; - -// Security Notification Settings -/** Security notification configuration. */ -export interface SecurityNotificationSettings { - enabled: boolean; - min_log_level: string; - notify_waf_blocks: boolean; - notify_acl_denials: boolean; - notify_rate_limit_hits: boolean; - webhook_url?: string; - email_recipients?: string; -} - -/** - * Fetches security notification settings. - * @returns Promise resolving to SecurityNotificationSettings - * @throws {AxiosError} If the request fails - */ -export const getSecurityNotificationSettings = async (): Promise => { - const response = await client.get('/notifications/settings/security'); - return response.data; -}; - -/** - * Updates security notification settings. - * @param settings - Partial settings to update - * @returns Promise resolving to the updated SecurityNotificationSettings - * @throws {AxiosError} If update fails - */ -export const updateSecurityNotificationSettings = async ( - settings: Partial -): Promise => { - const response = await client.put('/notifications/settings/security', settings); - return response.data; -}; diff --git a/frontend/src/api/presets.ts b/frontend/src/api/presets.ts deleted file mode 100644 index 6153ab21..00000000 --- a/frontend/src/api/presets.ts +++ /dev/null @@ -1,104 +0,0 @@ -import client from './client' - -/** Summary of an available CrowdSec preset. */ -export interface CrowdsecPresetSummary { - slug: string - title: string - summary: string - source: string - tags?: string[] - requires_hub: boolean - available: boolean - cached: boolean - cache_key?: string - etag?: string - retrieved_at?: string -} - -/** Response from pulling a CrowdSec preset. */ -export interface PullCrowdsecPresetResponse { - status: string - slug: string - preview: string - cache_key: string - etag?: string - retrieved_at?: string - source?: string -} - -/** Response from applying a CrowdSec preset. */ -export interface ApplyCrowdsecPresetResponse { - status: string - backup?: string - reload_hint?: boolean - used_cscli?: boolean - cache_key?: string - slug?: string -} - -/** Cached CrowdSec preset preview data. */ -export interface CachedCrowdsecPresetPreview { - preview: string - cache_key: string - etag?: string -} - -/** - * Lists all available CrowdSec presets. - * @returns Promise resolving to object containing presets array - * @throws {AxiosError} If the request fails - */ -export async function listCrowdsecPresets() { - const resp = await client.get<{ presets: CrowdsecPresetSummary[] }>('/admin/crowdsec/presets') - return resp.data -} - -/** - * Gets all CrowdSec presets (alias for listCrowdsecPresets). - * @returns Promise resolving to object containing presets array - * @throws {AxiosError} If the request fails - */ -export async function getCrowdsecPresets() { - return listCrowdsecPresets() -} - -/** - * Pulls a CrowdSec preset from the remote source. - * @param slug - The preset slug identifier - * @returns Promise resolving to PullCrowdsecPresetResponse with preview - * @throws {AxiosError} If pull fails or preset not found - */ -export async function pullCrowdsecPreset(slug: string) { - const resp = await client.post('/admin/crowdsec/presets/pull', { slug }) - return resp.data -} - -/** - * Applies a CrowdSec preset to the configuration. - * @param payload - Object with preset slug and optional cache_key - * @returns Promise resolving to ApplyCrowdsecPresetResponse - * @throws {AxiosError} If application fails - */ -export async function applyCrowdsecPreset(payload: { slug: string; cache_key?: string }) { - const resp = await client.post('/admin/crowdsec/presets/apply', payload) - return resp.data -} - -/** - * Gets a cached CrowdSec preset preview. - * @param slug - The preset slug identifier - * @returns Promise resolving to CachedCrowdsecPresetPreview - * @throws {AxiosError} If not cached or request fails - */ -export async function getCrowdsecPresetCache(slug: string) { - const resp = await client.get(`/admin/crowdsec/presets/cache/${encodeURIComponent(slug)}`) - return resp.data -} - -export default { - listCrowdsecPresets, - getCrowdsecPresets, - pullCrowdsecPreset, - applyCrowdsecPreset, - getCrowdsecPresetCache, -} diff --git a/frontend/src/api/proxyHosts.ts b/frontend/src/api/proxyHosts.ts deleted file mode 100644 index 70ea6e06..00000000 --- a/frontend/src/api/proxyHosts.ts +++ /dev/null @@ -1,182 +0,0 @@ -import client from './client'; - -export interface Location { - uuid?: string; - path: string; - forward_scheme: string; - forward_host: string; - forward_port: number; -} - -export interface Certificate { - id: number; - uuid: string; - name: string; - provider: string; - domains: string; - expires_at: string; -} - -export type ApplicationPreset = 'none' | 'plex' | 'jellyfin' | 'emby' | 'homeassistant' | 'nextcloud' | 'vaultwarden'; - -export interface ProxyHost { - uuid: string; - name: string; - domain_names: string; - forward_scheme: string; - forward_host: string; - forward_port: number; - ssl_forced: boolean; - http2_support: boolean; - hsts_enabled: boolean; - hsts_subdomains: boolean; - block_exploits: boolean; - websocket_support: boolean; - enable_standard_headers?: boolean; - forward_auth_enabled?: boolean; - waf_disabled?: boolean; - application: ApplicationPreset; - locations: Location[]; - advanced_config?: string; - advanced_config_backup?: string; - enabled: boolean; - certificate_id?: number | null; - certificate?: Certificate | null; - access_list_id?: number | null; - security_header_profile_id?: number | null; - dns_provider_id?: number | null; - security_header_profile?: { - id: number; - uuid: string; - name: string; - description: string; - security_score: number; - is_preset: boolean; - } | null; - created_at: string; - updated_at: string; -} - -/** - * Fetches all proxy hosts from the API. - * @returns Promise resolving to array of ProxyHost objects - * @throws {AxiosError} If the request fails - */ -export const getProxyHosts = async (): Promise => { - const { data } = await client.get('/proxy-hosts'); - return data; -}; - -/** - * Fetches a single proxy host by UUID. - * @param uuid - The unique identifier of the proxy host - * @returns Promise resolving to the ProxyHost object - * @throws {AxiosError} If the request fails or host not found - */ -export const getProxyHost = async (uuid: string): Promise => { - const { data } = await client.get(`/proxy-hosts/${uuid}`); - return data; -}; - -/** - * Creates a new proxy host. - * @param host - Partial ProxyHost object with configuration - * @returns Promise resolving to the created ProxyHost - * @throws {AxiosError} If the request fails or validation errors occur - */ -export const createProxyHost = async (host: Partial): Promise => { - const { data } = await client.post('/proxy-hosts', host); - return data; -}; - -/** - * Updates an existing proxy host. - * @param uuid - The unique identifier of the proxy host to update - * @param host - Partial ProxyHost object with fields to update - * @returns Promise resolving to the updated ProxyHost - * @throws {AxiosError} If the request fails or host not found - */ -export const updateProxyHost = async (uuid: string, host: Partial): Promise => { - const { data } = await client.put(`/proxy-hosts/${uuid}`, host); - return data; -}; - -/** - * Deletes a proxy host. - * @param uuid - The unique identifier of the proxy host to delete - * @param deleteUptime - Optional flag to also delete associated uptime monitors - * @throws {AxiosError} If the request fails or host not found - */ -export const deleteProxyHost = async (uuid: string, deleteUptime?: boolean): Promise => { - const url = `/proxy-hosts/${uuid}${deleteUptime ? '?delete_uptime=true' : ''}` - await client.delete(url); -}; - -/** - * Tests connectivity to a backend host. - * @param host - The hostname or IP address to test - * @param port - The port number to test - * @throws {AxiosError} If the connection test fails - */ -export const testProxyHostConnection = async (host: string, port: number): Promise => { - await client.post('/proxy-hosts/test', { forward_host: host, forward_port: port }); -}; - -export interface BulkUpdateACLRequest { - host_uuids: string[]; - access_list_id: number | null; -} - -export interface BulkUpdateACLResponse { - updated: number; - errors: { uuid: string; error: string }[]; -} - -/** - * Bulk updates access control list assignments for multiple proxy hosts. - * @param hostUUIDs - Array of proxy host UUIDs to update - * @param accessListID - The access list ID to assign, or null to remove - * @returns Promise resolving to the bulk update result with success/error counts - * @throws {AxiosError} If the request fails - */ -export const bulkUpdateACL = async ( - hostUUIDs: string[], - accessListID: number | null -): Promise => { - const { data } = await client.put('/proxy-hosts/bulk-update-acl', { - host_uuids: hostUUIDs, - access_list_id: accessListID, - }); - return data; -}; - -export interface BulkUpdateSecurityHeadersRequest { - host_uuids: string[]; - security_header_profile_id: number | null; -} - -export interface BulkUpdateSecurityHeadersResponse { - updated: number; - errors: { uuid: string; error: string }[]; -} - -/** - * Bulk updates security header profile assignments for multiple proxy hosts. - * @param hostUUIDs - Array of proxy host UUIDs to update - * @param securityHeaderProfileId - The security header profile ID to assign, or null to remove - * @returns Promise resolving to the bulk update result with success/error counts - * @throws {AxiosError} If the request fails - */ -export const bulkUpdateSecurityHeaders = async ( - hostUUIDs: string[], - securityHeaderProfileId: number | null -): Promise => { - const { data } = await client.put( - '/proxy-hosts/bulk-update-security-headers', - { - host_uuids: hostUUIDs, - security_header_profile_id: securityHeaderProfileId, - } - ); - return data; -}; diff --git a/frontend/src/api/remoteServers.ts b/frontend/src/api/remoteServers.ts deleted file mode 100644 index 14457bfc..00000000 --- a/frontend/src/api/remoteServers.ts +++ /dev/null @@ -1,94 +0,0 @@ -import client from './client'; - -/** Remote server configuration for Docker host connections. */ -export interface RemoteServer { - uuid: string; - name: string; - provider: string; - host: string; - port: number; - username?: string; - enabled: boolean; - reachable: boolean; - last_check?: string; - created_at: string; - updated_at: string; -} - -/** - * Fetches all remote servers. - * @param enabledOnly - If true, only returns enabled servers - * @returns Promise resolving to array of RemoteServer objects - * @throws {AxiosError} If the request fails - */ -export const getRemoteServers = async (enabledOnly = false): Promise => { - const params = enabledOnly ? { enabled: true } : {}; - const { data } = await client.get('/remote-servers', { params }); - return data; -}; - -/** - * Fetches a single remote server by UUID. - * @param uuid - The unique identifier of the remote server - * @returns Promise resolving to the RemoteServer object - * @throws {AxiosError} If the request fails or server not found - */ -export const getRemoteServer = async (uuid: string): Promise => { - const { data } = await client.get(`/remote-servers/${uuid}`); - return data; -}; - -/** - * Creates a new remote server. - * @param server - Partial RemoteServer configuration - * @returns Promise resolving to the created RemoteServer - * @throws {AxiosError} If creation fails - */ -export const createRemoteServer = async (server: Partial): Promise => { - const { data } = await client.post('/remote-servers', server); - return data; -}; - -/** - * Updates an existing remote server. - * @param uuid - The unique identifier of the server to update - * @param server - Partial RemoteServer with fields to update - * @returns Promise resolving to the updated RemoteServer - * @throws {AxiosError} If update fails or server not found - */ -export const updateRemoteServer = async (uuid: string, server: Partial): Promise => { - const { data } = await client.put(`/remote-servers/${uuid}`, server); - return data; -}; - -/** - * Deletes a remote server. - * @param uuid - The unique identifier of the server to delete - * @throws {AxiosError} If deletion fails or server not found - */ -export const deleteRemoteServer = async (uuid: string): Promise => { - await client.delete(`/remote-servers/${uuid}`); -}; - -/** - * Tests connectivity to an existing remote server. - * @param uuid - The unique identifier of the server to test - * @returns Promise resolving to object with server address - * @throws {AxiosError} If connection test fails - */ -export const testRemoteServerConnection = async (uuid: string): Promise<{ address: string }> => { - const { data } = await client.post<{ address: string }>(`/remote-servers/${uuid}/test`); - return data; -}; - -/** - * Tests connectivity to a custom host and port. - * @param host - The hostname or IP to test - * @param port - The port number to test - * @returns Promise resolving to connection result with reachable status - * @throws {AxiosError} If request fails - */ -export const testCustomRemoteServerConnection = async (host: string, port: number): Promise<{ address: string; reachable: boolean; error?: string }> => { - const { data } = await client.post<{ address: string; reachable: boolean; error?: string }>('/remote-servers/test', { host, port }); - return data; -}; diff --git a/frontend/src/api/security.ts b/frontend/src/api/security.ts deleted file mode 100644 index e1a304f4..00000000 --- a/frontend/src/api/security.ts +++ /dev/null @@ -1,189 +0,0 @@ -import client from './client' - -/** Security module status information. */ -export interface SecurityStatus { - cerberus?: { enabled: boolean } - crowdsec: { - mode: 'disabled' | 'local' - api_url: string - enabled: boolean - } - waf: { - mode: 'disabled' | 'enabled' - enabled: boolean - } - rate_limit: { - mode?: 'disabled' | 'enabled' - enabled: boolean - } - acl: { - enabled: boolean - } -} - -/** - * Gets the current security status for all modules. - * @returns Promise resolving to SecurityStatus - * @throws {AxiosError} If the request fails - */ -export const getSecurityStatus = async (): Promise => { - const response = await client.get('/security/status') - return response.data -} - -/** Security configuration payload. */ -export interface SecurityConfigPayload { - name?: string - enabled?: boolean - admin_whitelist?: string - crowdsec_mode?: string - crowdsec_api_url?: string - waf_mode?: string - waf_rules_source?: string - waf_learning?: boolean - rate_limit_enable?: boolean - rate_limit_burst?: number - rate_limit_requests?: number - rate_limit_window_sec?: number -} - -/** - * Gets the current security configuration. - * @returns Promise resolving to the security configuration - * @throws {AxiosError} If the request fails - */ -export const getSecurityConfig = async () => { - const response = await client.get('/security/config') - return response.data -} - -/** - * Updates security configuration. - * @param payload - SecurityConfigPayload with settings to update - * @returns Promise resolving to the updated configuration - * @throws {AxiosError} If update fails - */ -export const updateSecurityConfig = async (payload: SecurityConfigPayload) => { - const response = await client.post('/security/config', payload) - return response.data -} - -/** - * Generates a break-glass token for emergency access. - * @returns Promise resolving to object containing the token - * @throws {AxiosError} If generation fails - */ -export const generateBreakGlassToken = async () => { - const response = await client.post('/security/breakglass/generate') - return response.data -} - -/** - * Enables the Cerberus security module. - * @param payload - Optional configuration for enabling - * @returns Promise resolving to enable result - * @throws {AxiosError} If enabling fails - */ -export const enableCerberus = async (payload?: Record) => { - const response = await client.post('/security/enable', payload || {}) - return response.data -} - -/** - * Disables the Cerberus security module. - * @param payload - Optional configuration for disabling - * @returns Promise resolving to disable result - * @throws {AxiosError} If disabling fails - */ -export const disableCerberus = async (payload?: Record) => { - const response = await client.post('/security/disable', payload || {}) - return response.data -} - -/** - * Gets security decisions (bans, captchas) with optional limit. - * @param limit - Maximum number of decisions to return (default: 50) - * @returns Promise resolving to decisions list - * @throws {AxiosError} If the request fails - */ -export const getDecisions = async (limit = 50) => { - const response = await client.get(`/security/decisions?limit=${limit}`) - return response.data -} - -/** Payload for creating a security decision. */ -export interface CreateDecisionPayload { - type: string - value: string - duration: string - reason?: string -} - -/** - * Creates a new security decision (e.g., ban an IP). - * @param payload - Decision configuration - * @returns Promise resolving to the created decision - * @throws {AxiosError} If creation fails - */ -export const createDecision = async (payload: CreateDecisionPayload) => { - const response = await client.post('/security/decisions', payload) - return response.data -} - -// WAF Ruleset types -/** WAF security ruleset configuration. */ -export interface SecurityRuleSet { - id: number - uuid: string - name: string - source_url: string - mode: string - last_updated: string - content: string -} - -/** Response containing WAF rulesets. */ -export interface RuleSetsResponse { - rulesets: SecurityRuleSet[] -} - -/** Payload for creating/updating a WAF ruleset. */ -export interface UpsertRuleSetPayload { - id?: number - name: string - content?: string - source_url?: string - mode?: 'blocking' | 'detection' -} - -/** - * Gets all WAF rulesets. - * @returns Promise resolving to RuleSetsResponse - * @throws {AxiosError} If the request fails - */ -export const getRuleSets = async (): Promise => { - const response = await client.get('/security/rulesets') - return response.data -} - -/** - * Creates or updates a WAF ruleset. - * @param payload - Ruleset configuration - * @returns Promise resolving to the upserted ruleset - * @throws {AxiosError} If upsert fails - */ -export const upsertRuleSet = async (payload: UpsertRuleSetPayload) => { - const response = await client.post('/security/rulesets', payload) - return response.data -} - -/** - * Deletes a WAF ruleset. - * @param id - The ruleset ID to delete - * @returns Promise resolving to delete result - * @throws {AxiosError} If deletion fails or ruleset not found - */ -export const deleteRuleSet = async (id: number) => { - const response = await client.delete(`/security/rulesets/${id}`) - return response.data -} diff --git a/frontend/src/api/securityHeaders.ts b/frontend/src/api/securityHeaders.ts deleted file mode 100644 index 53138c57..00000000 --- a/frontend/src/api/securityHeaders.ts +++ /dev/null @@ -1,188 +0,0 @@ -import client from './client'; - -// Types -export interface SecurityHeaderProfile { - id: number; - uuid: string; - name: string; - hsts_enabled: boolean; - hsts_max_age: number; - hsts_include_subdomains: boolean; - hsts_preload: boolean; - csp_enabled: boolean; - csp_directives: string; - csp_report_only: boolean; - csp_report_uri: string; - x_frame_options: string; - x_content_type_options: boolean; - referrer_policy: string; - permissions_policy: string; - cross_origin_opener_policy: string; - cross_origin_resource_policy: string; - cross_origin_embedder_policy: string; - xss_protection: boolean; - cache_control_no_store: boolean; - security_score: number; - is_preset: boolean; - preset_type: string; - description: string; - created_at: string; - updated_at: string; -} - -export interface SecurityHeaderPreset { - preset_type: 'basic' | 'strict' | 'paranoid'; - name: string; - description: string; - security_score: number; - config: Partial; -} - -export interface ScoreBreakdown { - score: number; - max_score: number; - breakdown: Record; - suggestions: string[]; -} - -export interface CSPDirective { - directive: string; - values: string[]; -} - -export interface CreateProfileRequest { - name: string; - description?: string; - hsts_enabled?: boolean; - hsts_max_age?: number; - hsts_include_subdomains?: boolean; - hsts_preload?: boolean; - csp_enabled?: boolean; - csp_directives?: string; - csp_report_only?: boolean; - csp_report_uri?: string; - x_frame_options?: string; - x_content_type_options?: boolean; - referrer_policy?: string; - permissions_policy?: string; - cross_origin_opener_policy?: string; - cross_origin_resource_policy?: string; - cross_origin_embedder_policy?: string; - xss_protection?: boolean; - cache_control_no_store?: boolean; -} - -export interface ApplyPresetRequest { - preset_type: string; - name: string; -} - -// API Functions -export const securityHeadersApi = { - /** - * Lists all security header profiles. - * @returns Promise resolving to array of SecurityHeaderProfile objects - * @throws {AxiosError} If the request fails - */ - async listProfiles(): Promise { - const response = await client.get<{profiles: SecurityHeaderProfile[]}>('/security/headers/profiles'); - return response.data.profiles; - }, - - /** - * Gets a single security header profile by ID or UUID. - * @param id - The profile ID (number) or UUID (string) - * @returns Promise resolving to the SecurityHeaderProfile object - * @throws {AxiosError} If the request fails or profile not found - */ - async getProfile(id: number | string): Promise { - const response = await client.get<{profile: SecurityHeaderProfile}>(`/security/headers/profiles/${id}`); - return response.data.profile; - }, - - /** - * Creates a new security header profile. - * @param data - CreateProfileRequest with profile configuration - * @returns Promise resolving to the created SecurityHeaderProfile - * @throws {AxiosError} If creation fails or validation errors occur - */ - async createProfile(data: CreateProfileRequest): Promise { - const response = await client.post<{profile: SecurityHeaderProfile}>('/security/headers/profiles', data); - return response.data.profile; - }, - - /** - * Updates an existing security header profile. - * @param id - The profile ID to update - * @param data - Partial CreateProfileRequest with fields to update - * @returns Promise resolving to the updated SecurityHeaderProfile - * @throws {AxiosError} If update fails or profile not found - */ - async updateProfile(id: number, data: Partial): Promise { - const response = await client.put<{profile: SecurityHeaderProfile}>(`/security/headers/profiles/${id}`, data); - return response.data.profile; - }, - - /** - * Deletes a security header profile. - * @param id - The profile ID to delete (cannot delete preset profiles) - * @throws {AxiosError} If deletion fails, profile not found, or is a preset - */ - async deleteProfile(id: number): Promise { - await client.delete(`/security/headers/profiles/${id}`); - }, - - /** - * Gets all built-in security header presets. - * @returns Promise resolving to array of SecurityHeaderPreset objects - * @throws {AxiosError} If the request fails - */ - async getPresets(): Promise { - const response = await client.get<{presets: SecurityHeaderPreset[]}>('/security/headers/presets'); - return response.data.presets; - }, - - /** - * Applies a preset to create or update a security header profile. - * @param data - ApplyPresetRequest with preset type and profile name - * @returns Promise resolving to the created/updated SecurityHeaderProfile - * @throws {AxiosError} If preset application fails - */ - async applyPreset(data: ApplyPresetRequest): Promise { - const response = await client.post<{profile: SecurityHeaderProfile}>('/security/headers/presets/apply', data); - return response.data.profile; - }, - - /** - * Calculates the security score for given header settings. - * @param config - Partial CreateProfileRequest with settings to evaluate - * @returns Promise resolving to ScoreBreakdown with score, max, breakdown, and suggestions - * @throws {AxiosError} If calculation fails - */ - async calculateScore(config: Partial): Promise { - const response = await client.post('/security/headers/score', config); - return response.data; - }, - - /** - * Validates a Content Security Policy string. - * @param csp - The CSP string to validate - * @returns Promise resolving to object with validity status and any errors - * @throws {AxiosError} If validation request fails - */ - async validateCSP(csp: string): Promise<{ valid: boolean; errors: string[] }> { - const response = await client.post<{ valid: boolean; errors: string[] }>('/security/headers/csp/validate', { csp }); - return response.data; - }, - - /** - * Builds a Content Security Policy string from directives. - * @param directives - Array of CSPDirective objects to combine - * @returns Promise resolving to object containing the built CSP string - * @throws {AxiosError} If build request fails - */ - async buildCSP(directives: CSPDirective[]): Promise<{ csp: string }> { - const response = await client.post<{ csp: string }>('/security/headers/csp/build', { directives }); - return response.data; - }, -}; diff --git a/frontend/src/api/settings.ts b/frontend/src/api/settings.ts deleted file mode 100644 index 42f46dc4..00000000 --- a/frontend/src/api/settings.ts +++ /dev/null @@ -1,57 +0,0 @@ -import client from './client' - -/** Map of setting keys to string values. */ -export interface SettingsMap { - [key: string]: string -} - -/** - * Fetches all application settings. - * @returns Promise resolving to SettingsMap - * @throws {AxiosError} If the request fails - */ -export const getSettings = async (): Promise => { - const response = await client.get('/settings') - return response.data -} - -/** - * Updates a single application setting. - * @param key - The setting key to update - * @param value - The new value for the setting - * @param category - Optional category for organization - * @param type - Optional type hint for the setting - * @throws {AxiosError} If the update fails - */ -export const updateSetting = async (key: string, value: string, category?: string, type?: string): Promise => { - await client.post('/settings', { key, value, category, type }) -} - -/** - * Validates a URL for use as the application URL. - * @param url - The URL to validate - * @returns Promise resolving to validation result - */ -export const validatePublicURL = async (url: string): Promise<{ - valid: boolean - normalized?: string - error?: string -}> => { - const response = await client.post('/settings/validate-url', { url }) - return response.data -} - -/** - * Tests if a URL is reachable from the server with SSRF protection. - * @param url - The URL to test - * @returns Promise resolving to test result with reachability status and latency - */ -export const testPublicURL = async (url: string): Promise<{ - reachable: boolean - latency?: number - message?: string - error?: string -}> => { - const response = await client.post('/settings/test-url', { url }) - return response.data -} diff --git a/frontend/src/api/setup.ts b/frontend/src/api/setup.ts deleted file mode 100644 index fb85a97c..00000000 --- a/frontend/src/api/setup.ts +++ /dev/null @@ -1,32 +0,0 @@ -import client from './client'; - -/** Status indicating if initial setup is required. */ -export interface SetupStatus { - setupRequired: boolean; -} - -/** Request payload for initial setup. */ -export interface SetupRequest { - name: string; - email: string; - password: string; -} - -/** - * Checks if initial setup is required. - * @returns Promise resolving to SetupStatus - * @throws {AxiosError} If the request fails - */ -export const getSetupStatus = async (): Promise => { - const response = await client.get('/setup'); - return response.data; -}; - -/** - * Performs initial application setup with admin user creation. - * @param data - SetupRequest with admin user details - * @throws {AxiosError} If setup fails or already completed - */ -export const performSetup = async (data: SetupRequest): Promise => { - await client.post('/setup', data); -}; diff --git a/frontend/src/api/smtp.ts b/frontend/src/api/smtp.ts deleted file mode 100644 index 434e1e85..00000000 --- a/frontend/src/api/smtp.ts +++ /dev/null @@ -1,76 +0,0 @@ -import client from './client' - -/** SMTP server configuration. */ -export interface SMTPConfig { - host: string - port: number - username: string - password: string - from_address: string - encryption: 'none' | 'ssl' | 'starttls' - configured: boolean -} - -/** Request payload for SMTP configuration. */ -export interface SMTPConfigRequest { - host: string - port: number - username: string - password: string - from_address: string - encryption: 'none' | 'ssl' | 'starttls' -} - -/** Request payload for sending a test email. */ -export interface TestEmailRequest { - to: string -} - -/** Result of an SMTP test operation. */ -export interface SMTPTestResult { - success: boolean - message?: string - error?: string -} - -/** - * Fetches the current SMTP configuration. - * @returns Promise resolving to SMTPConfig - * @throws {AxiosError} If the request fails - */ -export const getSMTPConfig = async (): Promise => { - const response = await client.get('/settings/smtp') - return response.data -} - -/** - * Updates the SMTP configuration. - * @param config - SMTPConfigRequest with new settings - * @returns Promise resolving to success message - * @throws {AxiosError} If update fails - */ -export const updateSMTPConfig = async (config: SMTPConfigRequest): Promise<{ message: string }> => { - const response = await client.post<{ message: string }>('/settings/smtp', config) - return response.data -} - -/** - * Tests the SMTP connection with current settings. - * @returns Promise resolving to SMTPTestResult - * @throws {AxiosError} If test request fails - */ -export const testSMTPConnection = async (): Promise => { - const response = await client.post('/settings/smtp/test') - return response.data -} - -/** - * Sends a test email to verify SMTP configuration. - * @param request - TestEmailRequest with recipient address - * @returns Promise resolving to SMTPTestResult - * @throws {AxiosError} If sending fails - */ -export const sendTestEmail = async (request: TestEmailRequest): Promise => { - const response = await client.post('/settings/smtp/test-email', request) - return response.data -} diff --git a/frontend/src/api/system.ts b/frontend/src/api/system.ts deleted file mode 100644 index 9276d3e4..00000000 --- a/frontend/src/api/system.ts +++ /dev/null @@ -1,72 +0,0 @@ -import client from './client'; - -/** Update availability information. */ -export interface UpdateInfo { - available: boolean; - latest_version: string; - changelog_url: string; -} - -/** System notification entry. */ -export interface Notification { - id: string; - type: 'info' | 'success' | 'warning' | 'error'; - title: string; - message: string; - read: boolean; - created_at: string; -} - -/** - * Checks for available application updates. - * @returns Promise resolving to UpdateInfo - * @throws {AxiosError} If the request fails - */ -export const checkUpdates = async (): Promise => { - const response = await client.get('/system/updates'); - return response.data; -}; - -/** - * Fetches system notifications. - * @param unreadOnly - If true, only returns unread notifications - * @returns Promise resolving to array of Notification objects - * @throws {AxiosError} If the request fails - */ -export const getNotifications = async (unreadOnly = false): Promise => { - const response = await client.get('/notifications', { params: { unread: unreadOnly } }); - return response.data; -}; - -/** - * Marks a notification as read. - * @param id - The notification ID to mark as read - * @throws {AxiosError} If marking fails or notification not found - */ -export const markNotificationRead = async (id: string): Promise => { - await client.post(`/notifications/${id}/read`); -}; - -/** - * Marks all notifications as read. - * @throws {AxiosError} If the request fails - */ -export const markAllNotificationsRead = async (): Promise => { - await client.post('/notifications/read-all'); -}; - -/** Response containing the client's public IP address. */ -export interface MyIPResponse { - ip: string; - source: string; -} - -/** - * Gets the client's public IP address as seen by the server. - * @returns Promise resolving to MyIPResponse with IP address - * @throws {AxiosError} If the request fails - */ -export const getMyIP = async (): Promise => { - const response = await client.get('/system/my-ip'); - return response.data; -}; diff --git a/frontend/src/api/uptime.ts b/frontend/src/api/uptime.ts deleted file mode 100644 index 9ed0a09d..00000000 --- a/frontend/src/api/uptime.ts +++ /dev/null @@ -1,95 +0,0 @@ -import client from './client'; - -/** Uptime monitor configuration. */ -export interface UptimeMonitor { - id: string; - upstream_host?: string; - proxy_host_id?: number; - remote_server_id?: number; - name: string; - type: string; - url: string; - interval: number; - enabled: boolean; - status: string; - last_check?: string | null; - latency: number; - max_retries: number; -} - -/** Uptime heartbeat (check result) entry. */ -export interface UptimeHeartbeat { - id: number; - monitor_id: string; - status: string; - latency: number; - message: string; - created_at: string; -} - -/** - * Fetches all uptime monitors. - * @returns Promise resolving to array of UptimeMonitor objects - * @throws {AxiosError} If the request fails - */ -export const getMonitors = async () => { - const response = await client.get('/uptime/monitors'); - return response.data; -}; - -/** - * Fetches heartbeat history for a monitor. - * @param id - The monitor ID - * @param limit - Maximum number of heartbeats to return (default: 50) - * @returns Promise resolving to array of UptimeHeartbeat objects - * @throws {AxiosError} If the request fails or monitor not found - */ -export const getMonitorHistory = async (id: string, limit: number = 50) => { - const response = await client.get(`/uptime/monitors/${id}/history?limit=${limit}`); - return response.data; -}; - -/** - * Updates an uptime monitor configuration. - * @param id - The monitor ID to update - * @param data - Partial UptimeMonitor with fields to update - * @returns Promise resolving to the updated UptimeMonitor - * @throws {AxiosError} If update fails or monitor not found - */ -export const updateMonitor = async (id: string, data: Partial) => { - const response = await client.put(`/uptime/monitors/${id}`, data); - return response.data; -}; - -/** - * Deletes an uptime monitor. - * @param id - The monitor ID to delete - * @returns Promise resolving to void - * @throws {AxiosError} If deletion fails or monitor not found - */ -export const deleteMonitor = async (id: string) => { - const response = await client.delete(`/uptime/monitors/${id}`); - return response.data; -}; - -/** - * Syncs monitors with proxy hosts and remote servers. - * @param body - Optional configuration for sync (interval, max_retries) - * @returns Promise resolving to sync result - * @throws {AxiosError} If sync fails - */ -export async function syncMonitors(body?: { interval?: number; max_retries?: number }) { - const res = await client.post('/uptime/sync', body || {}); - return res.data; -} - -/** - * Triggers an immediate check for a monitor. - * @param id - The monitor ID to check - * @returns Promise resolving to object with result message - * @throws {AxiosError} If check fails or monitor not found - */ -export const checkMonitor = async (id: string) => { - const response = await client.post<{ message: string }>(`/uptime/monitors/${id}/check`); - return response.data; -}; diff --git a/frontend/src/api/user.ts b/frontend/src/api/user.ts deleted file mode 100644 index d3cd3f11..00000000 --- a/frontend/src/api/user.ts +++ /dev/null @@ -1,41 +0,0 @@ -import client from './client' - -/** Current user profile information. */ -export interface UserProfile { - id: number - email: string - name: string - role: string - api_key: string -} - -/** - * Fetches the current user's profile. - * @returns Promise resolving to UserProfile - * @throws {AxiosError} If the request fails or not authenticated - */ -export const getProfile = async (): Promise => { - const response = await client.get('/user/profile') - return response.data -} - -/** - * Regenerates the current user's API key. - * @returns Promise resolving to object containing the new API key - * @throws {AxiosError} If regeneration fails - */ -export const regenerateApiKey = async (): Promise<{ api_key: string }> => { - const response = await client.post('/user/api-key') - return response.data -} - -/** - * Updates the current user's profile. - * @param data - Object with name, email, and optional current_password for verification - * @returns Promise resolving to success message - * @throws {AxiosError} If update fails or password verification fails - */ -export const updateProfile = async (data: { name: string; email: string; current_password?: string }): Promise<{ message: string }> => { - const response = await client.post('/user/profile', data) - return response.data -} diff --git a/frontend/src/api/users.test.ts b/frontend/src/api/users.test.ts deleted file mode 100644 index 06ed6ffc..00000000 --- a/frontend/src/api/users.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' -import client from './client' -import { - listUsers, - getUser, - createUser, - inviteUser, - updateUser, - deleteUser, - updateUserPermissions, - validateInvite, - acceptInvite, -} from './users' - -vi.mock('./client', () => ({ - default: { - get: vi.fn(), - post: vi.fn(), - put: vi.fn(), - delete: vi.fn(), - }, -})) - -const mockedClient = client as unknown as { - get: ReturnType - post: ReturnType - put: ReturnType - delete: ReturnType -} - -describe('users api', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it('lists and fetches users', async () => { - mockedClient.get - .mockResolvedValueOnce({ data: [{ id: 1, uuid: 'u1', email: 'a@example.com', name: 'A', role: 'admin', enabled: true, permission_mode: 'allow_all', created_at: '', updated_at: '' }] }) - .mockResolvedValueOnce({ data: { id: 2, uuid: 'u2', email: 'b@example.com', name: 'B', role: 'user', enabled: true, permission_mode: 'allow_all', created_at: '', updated_at: '' } }) - - const users = await listUsers() - expect(mockedClient.get).toHaveBeenCalledWith('/users') - expect(users[0].email).toBe('a@example.com') - - const user = await getUser(2) - expect(mockedClient.get).toHaveBeenCalledWith('/users/2') - expect(user.uuid).toBe('u2') - }) - - it('creates, invites, updates, and deletes users', async () => { - mockedClient.post - .mockResolvedValueOnce({ data: { id: 3, uuid: 'u3', email: 'c@example.com', name: 'C', role: 'user', enabled: true, permission_mode: 'allow_all', created_at: '', updated_at: '' } }) - .mockResolvedValueOnce({ data: { id: 4, uuid: 'u4', email: 'invite@example.com', role: 'user', invite_token: 'token', email_sent: true, expires_at: '' } }) - - mockedClient.put.mockResolvedValueOnce({ data: { message: 'updated' } }) - mockedClient.delete.mockResolvedValueOnce({ data: { message: 'deleted' } }) - - const created = await createUser({ email: 'c@example.com', name: 'C', password: 'pw' }) - expect(mockedClient.post).toHaveBeenCalledWith('/users', { email: 'c@example.com', name: 'C', password: 'pw' }) - expect(created.id).toBe(3) - - const invite = await inviteUser({ email: 'invite@example.com', role: 'user' }) - expect(mockedClient.post).toHaveBeenCalledWith('/users/invite', { email: 'invite@example.com', role: 'user' }) - expect(invite.invite_token).toBe('token') - - await updateUser(3, { enabled: false }) - expect(mockedClient.put).toHaveBeenCalledWith('/users/3', { enabled: false }) - - await deleteUser(3) - expect(mockedClient.delete).toHaveBeenCalledWith('/users/3') - }) - - it('updates permissions and validates/accepts invites', async () => { - mockedClient.put.mockResolvedValueOnce({ data: { message: 'perms updated' } }) - mockedClient.get.mockResolvedValueOnce({ data: { valid: true, email: 'invite@example.com' } }) - mockedClient.post.mockResolvedValueOnce({ data: { message: 'accepted', email: 'invite@example.com' } }) - - const perms = await updateUserPermissions(5, { permission_mode: 'deny_all', permitted_hosts: [1, 2] }) - expect(mockedClient.put).toHaveBeenCalledWith('/users/5/permissions', { - permission_mode: 'deny_all', - permitted_hosts: [1, 2], - }) - expect(perms.message).toBe('perms updated') - - const validation = await validateInvite('token-abc') - expect(mockedClient.get).toHaveBeenCalledWith('/invite/validate', { params: { token: 'token-abc' } }) - expect(validation.valid).toBe(true) - - const accept = await acceptInvite({ token: 'token-abc', name: 'New', password: 'pw' }) - expect(mockedClient.post).toHaveBeenCalledWith('/invite/accept', { token: 'token-abc', name: 'New', password: 'pw' }) - expect(accept.message).toBe('accepted') - }) -}) diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts deleted file mode 100644 index 7eaea2fc..00000000 --- a/frontend/src/api/users.ts +++ /dev/null @@ -1,203 +0,0 @@ -import client from './client' - -/** User permission mode type. */ -export type PermissionMode = 'allow_all' | 'deny_all' - -/** User account information. */ -export interface User { - id: number - uuid: string - email: string - name: string - role: 'admin' | 'user' | 'viewer' - enabled: boolean - last_login?: string - invite_status?: 'pending' | 'accepted' | 'expired' - invited_at?: string - permission_mode: PermissionMode - permitted_hosts?: number[] - created_at: string - updated_at: string -} - -/** Request payload for creating a user. */ -export interface CreateUserRequest { - email: string - name: string - password: string - role?: string - permission_mode?: PermissionMode - permitted_hosts?: number[] -} - -/** Request payload for inviting a user. */ -export interface InviteUserRequest { - email: string - role?: string - permission_mode?: PermissionMode - permitted_hosts?: number[] -} - -/** Response from user invitation. */ -export interface InviteUserResponse { - id: number - uuid: string - email: string - role: string - invite_token: string - email_sent: boolean - expires_at: string -} - -/** Request payload for updating a user. */ -export interface UpdateUserRequest { - name?: string - email?: string - role?: string - enabled?: boolean -} - -/** Request payload for updating user permissions. */ -export interface UpdateUserPermissionsRequest { - permission_mode: PermissionMode - permitted_hosts: number[] -} - -/** Response from invite validation. */ -export interface ValidateInviteResponse { - valid: boolean - email: string -} - -/** Request payload for accepting an invitation. */ -export interface AcceptInviteRequest { - token: string - name: string - password: string -} - -/** - * Lists all users. - * @returns Promise resolving to array of User objects - * @throws {AxiosError} If the request fails - */ -export const listUsers = async (): Promise => { - const response = await client.get('/users') - return response.data -} - -/** - * Fetches a single user by ID. - * @param id - The user ID - * @returns Promise resolving to the User object - * @throws {AxiosError} If the request fails or user not found - */ -export const getUser = async (id: number): Promise => { - const response = await client.get(`/users/${id}`) - return response.data -} - -/** - * Creates a new user. - * @param data - CreateUserRequest with user details - * @returns Promise resolving to the created User - * @throws {AxiosError} If creation fails or email already exists - */ -export const createUser = async (data: CreateUserRequest): Promise => { - const response = await client.post('/users', data) - return response.data -} - -/** - * Invites a new user via email. - * @param data - InviteUserRequest with invitation details - * @returns Promise resolving to InviteUserResponse with token - * @throws {AxiosError} If invitation fails - */ -export const inviteUser = async (data: InviteUserRequest): Promise => { - const response = await client.post('/users/invite', data) - return response.data -} - -/** - * Updates an existing user. - * @param id - The user ID to update - * @param data - UpdateUserRequest with fields to update - * @returns Promise resolving to success message - * @throws {AxiosError} If update fails or user not found - */ -export const updateUser = async (id: number, data: UpdateUserRequest): Promise<{ message: string }> => { - const response = await client.put<{ message: string }>(`/users/${id}`, data) - return response.data -} - -/** - * Deletes a user. - * @param id - The user ID to delete - * @returns Promise resolving to success message - * @throws {AxiosError} If deletion fails or user not found - */ -export const deleteUser = async (id: number): Promise<{ message: string }> => { - const response = await client.delete<{ message: string }>(`/users/${id}`) - return response.data -} - -/** - * Updates a user's permissions. - * @param id - The user ID to update - * @param data - UpdateUserPermissionsRequest with new permissions - * @returns Promise resolving to success message - * @throws {AxiosError} If update fails or user not found - */ -export const updateUserPermissions = async ( - id: number, - data: UpdateUserPermissionsRequest -): Promise<{ message: string }> => { - const response = await client.put<{ message: string }>(`/users/${id}/permissions`, data) - return response.data -} - -// Public endpoints (no auth required) -/** - * Validates an invitation token. - * @param token - The invitation token to validate - * @returns Promise resolving to ValidateInviteResponse - * @throws {AxiosError} If validation fails - */ -export const validateInvite = async (token: string): Promise => { - const response = await client.get('/invite/validate', { - params: { token } - }) - return response.data -} - -/** - * Accepts an invitation and creates the user account. - * @param data - AcceptInviteRequest with token and user details - * @returns Promise resolving to success message and email - * @throws {AxiosError} If acceptance fails or token invalid/expired - */ -export const acceptInvite = async (data: AcceptInviteRequest): Promise<{ message: string; email: string }> => { - const response = await client.post<{ message: string; email: string }>('/invite/accept', data) - return response.data -} - -/** Response from invite URL preview. */ -export interface PreviewInviteURLResponse { - preview_url: string - base_url: string - is_configured: boolean - email: string - warning: boolean - warning_message: string -} - -/** - * Previews what the invite URL will look like for a given email. - * @param email - The email to preview - * @returns Promise resolving to PreviewInviteURLResponse - */ -export const previewInviteURL = async (email: string): Promise => { - const response = await client.post('/users/preview-invite-url', { email }) - return response.data -} diff --git a/frontend/src/api/websocket.ts b/frontend/src/api/websocket.ts deleted file mode 100644 index 7ae2da72..00000000 --- a/frontend/src/api/websocket.ts +++ /dev/null @@ -1,47 +0,0 @@ -import client from './client'; - -/** Information about a WebSocket connection. */ -export interface ConnectionInfo { - id: string; - type: 'logs' | 'cerberus'; - connected_at: string; - last_activity_at: string; - remote_addr?: string; - user_agent?: string; - filters?: string; -} - -/** Aggregate statistics for WebSocket connections. */ -export interface ConnectionStats { - total_active: number; - logs_connections: number; - cerberus_connections: number; - oldest_connection?: string; - last_updated: string; -} - -/** Response containing WebSocket connections list. */ -export interface ConnectionsResponse { - connections: ConnectionInfo[]; - count: number; -} - -/** - * Gets all active WebSocket connections. - * @returns Promise resolving to ConnectionsResponse with connections list - * @throws {AxiosError} If the request fails - */ -export const getWebSocketConnections = async (): Promise => { - const response = await client.get('/websocket/connections'); - return response.data; -}; - -/** - * Gets aggregate WebSocket connection statistics. - * @returns Promise resolving to ConnectionStats - * @throws {AxiosError} If the request fails - */ -export const getWebSocketStats = async (): Promise => { - const response = await client.get('/websocket/stats'); - return response.data; -}; diff --git a/frontend/src/components/AccessListForm.tsx b/frontend/src/components/AccessListForm.tsx deleted file mode 100644 index 86155b15..00000000 --- a/frontend/src/components/AccessListForm.tsx +++ /dev/null @@ -1,555 +0,0 @@ -import { useState } from 'react'; -import { Button } from './ui/Button'; -import { Input } from './ui/Input'; -import { Switch } from './ui/Switch'; -import { X, Plus, ExternalLink, Shield, AlertTriangle, Info, Download, Trash2 } from 'lucide-react'; -import type { AccessList, AccessListRule } from '../api/accessLists'; -import { SECURITY_PRESETS, calculateTotalIPs, formatIPCount, type SecurityPreset } from '../data/securityPresets'; -import { getMyIP } from '../api/system'; -import toast from 'react-hot-toast'; - -interface AccessListFormProps { - initialData?: AccessList; - onSubmit: (data: AccessListFormData) => void; - onCancel: () => void; - onDelete?: () => void; - isLoading?: boolean; - isDeleting?: boolean; -} - -export interface AccessListFormData { - name: string; - description: string; - type: 'whitelist' | 'blacklist' | 'geo_whitelist' | 'geo_blacklist'; - ip_rules: string; - country_codes: string; - local_network_only: boolean; - enabled: boolean; -} - -const COUNTRIES = [ - { code: 'US', name: 'United States' }, - { code: 'CA', name: 'Canada' }, - { code: 'GB', name: 'United Kingdom' }, - { code: 'DE', name: 'Germany' }, - { code: 'FR', name: 'France' }, - { code: 'IT', name: 'Italy' }, - { code: 'ES', name: 'Spain' }, - { code: 'NL', name: 'Netherlands' }, - { code: 'BE', name: 'Belgium' }, - { code: 'SE', name: 'Sweden' }, - { code: 'NO', name: 'Norway' }, - { code: 'DK', name: 'Denmark' }, - { code: 'FI', name: 'Finland' }, - { code: 'PL', name: 'Poland' }, - { code: 'CZ', name: 'Czech Republic' }, - { code: 'AT', name: 'Austria' }, - { code: 'CH', name: 'Switzerland' }, - { code: 'AU', name: 'Australia' }, - { code: 'NZ', name: 'New Zealand' }, - { code: 'JP', name: 'Japan' }, - { code: 'CN', name: 'China' }, - { code: 'IN', name: 'India' }, - { code: 'BR', name: 'Brazil' }, - { code: 'MX', name: 'Mexico' }, - { code: 'AR', name: 'Argentina' }, - { code: 'RU', name: 'Russia' }, - { code: 'UA', name: 'Ukraine' }, - { code: 'TR', name: 'Turkey' }, - { code: 'IL', name: 'Israel' }, - { code: 'SA', name: 'Saudi Arabia' }, - { code: 'AE', name: 'United Arab Emirates' }, - { code: 'EG', name: 'Egypt' }, - { code: 'ZA', name: 'South Africa' }, - { code: 'KR', name: 'South Korea' }, - { code: 'SG', name: 'Singapore' }, - { code: 'MY', name: 'Malaysia' }, - { code: 'TH', name: 'Thailand' }, - { code: 'ID', name: 'Indonesia' }, - { code: 'PH', name: 'Philippines' }, - { code: 'VN', name: 'Vietnam' }, -]; - -export function AccessListForm({ initialData, onSubmit, onCancel, onDelete, isLoading, isDeleting }: AccessListFormProps) { - const [formData, setFormData] = useState({ - name: initialData?.name || '', - description: initialData?.description || '', - type: initialData?.type || 'whitelist', - ip_rules: initialData?.ip_rules || '', - country_codes: initialData?.country_codes || '', - local_network_only: initialData?.local_network_only || false, - enabled: initialData?.enabled ?? true, - }); - - const [ipRules, setIPRules] = useState(() => { - if (initialData?.ip_rules) { - try { - return JSON.parse(initialData.ip_rules); - } catch { - return []; - } - } - return []; - }); - - const [selectedCountries, setSelectedCountries] = useState(() => { - if (initialData?.country_codes) { - return initialData.country_codes.split(',').map((c) => c.trim()); - } - return []; - }); - - const [newIP, setNewIP] = useState(''); - const [newIPDescription, setNewIPDescription] = useState(''); - const [showPresets, setShowPresets] = useState(false); - const [loadingMyIP, setLoadingMyIP] = useState(false); - - const isGeoType = formData.type.startsWith('geo_'); - const isIPType = !isGeoType; - - // Calculate total IPs in current rules - const totalIPs = isIPType && !formData.local_network_only - ? calculateTotalIPs(ipRules.map(r => r.cidr)) - : 0; - - const handleAddIP = () => { - if (!newIP.trim()) return; - - const newRule: AccessListRule = { - cidr: newIP.trim(), - description: newIPDescription.trim(), - }; - - const updatedRules = [...ipRules, newRule]; - setIPRules(updatedRules); - setNewIP(''); - setNewIPDescription(''); - }; - - const handleRemoveIP = (index: number) => { - setIPRules(ipRules.filter((_, i) => i !== index)); - }; - - const handleAddCountry = (countryCode: string) => { - if (!selectedCountries.includes(countryCode)) { - setSelectedCountries([...selectedCountries, countryCode]); - } - }; - - const handleRemoveCountry = (countryCode: string) => { - setSelectedCountries(selectedCountries.filter((c) => c !== countryCode)); - }; - - const handleApplyPreset = (preset: SecurityPreset) => { - if (preset.type === 'geo_blacklist' && preset.countryCodes) { - setFormData({ ...formData, type: 'geo_blacklist' }); - setSelectedCountries([...new Set([...selectedCountries, ...preset.countryCodes])]); - toast.success(`Applied preset: ${preset.name}`); - } else if (preset.type === 'blacklist' && preset.ipRanges) { - setFormData({ ...formData, type: 'blacklist' }); - const newRules = preset.ipRanges.filter( - (newRule) => !ipRules.some((existing) => existing.cidr === newRule.cidr) - ); - setIPRules([...ipRules, ...newRules]); - toast.success(`Applied preset: ${preset.name} (${newRules.length} rules added)`); - } - setShowPresets(false); - }; - - const handleGetMyIP = async () => { - setLoadingMyIP(true); - try { - const result = await getMyIP(); - setNewIP(result.ip); - toast.success(`Your IP: ${result.ip} (from ${result.source})`); - } catch { - toast.error('Failed to fetch your IP address'); - } finally { - setLoadingMyIP(false); - } - }; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - const data: AccessListFormData = { - ...formData, - ip_rules: isIPType && !formData.local_network_only ? JSON.stringify(ipRules) : '', - country_codes: isGeoType ? selectedCountries.join(',') : '', - }; - - onSubmit(data); - }; - - return ( -
- {/* Basic Info */} -
-
- - setFormData({ ...formData, name: e.target.value })} - placeholder="My Access List" - required - /> -
- -
- -