From af164c1f8c4745ca732c04163862569a64aa1bfd Mon Sep 17 00:00:00 2001 From: ale Date: Sat, 16 Aug 2025 23:07:29 +0200 Subject: [PATCH] initial commit Signed-off-by: ale --- .gitignore | 2 + EXAMPLES.md | 415 +++++++ README.md | 67 +- package-lock.json | 1670 --------------------------- package.json | 8 +- src/app/api/ping/route.js | 166 +++ src/app/api/status/route.js | 69 ++ src/app/components/CopyButton.js | 45 + src/app/components/PingForm.js | 137 +++ src/app/components/PingInterface.js | 111 ++ src/app/components/PingResults.js | 176 +++ src/app/components/RateLimitInfo.js | 116 ++ src/app/page.js | 116 +- src/hooks/usePingService.js | 108 ++ src/lib/config.js | 119 ++ src/lib/rate-limiter.js | 100 ++ src/lib/validators.js | 149 +++ src/middleware.js | 38 + 18 files changed, 1824 insertions(+), 1788 deletions(-) create mode 100644 EXAMPLES.md delete mode 100644 package-lock.json create mode 100644 src/app/api/ping/route.js create mode 100644 src/app/api/status/route.js create mode 100644 src/app/components/CopyButton.js create mode 100644 src/app/components/PingForm.js create mode 100644 src/app/components/PingInterface.js create mode 100644 src/app/components/PingResults.js create mode 100644 src/app/components/RateLimitInfo.js create mode 100644 src/hooks/usePingService.js create mode 100644 src/lib/config.js create mode 100644 src/lib/rate-limiter.js create mode 100644 src/lib/validators.js create mode 100644 src/middleware.js diff --git a/.gitignore b/.gitignore index 5ef6a52..106636f 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +*.lock +*-lock.json \ No newline at end of file diff --git a/EXAMPLES.md b/EXAMPLES.md new file mode 100644 index 0000000..d40f00f --- /dev/null +++ b/EXAMPLES.md @@ -0,0 +1,415 @@ +# Ejemplos de Uso - API Ping Service + +Este documento contiene ejemplos prácticos de cómo usar la API Ping Service en diferentes escenarios. + +## 📡 Ejemplos de API + +### 1. Ping Básico + +```bash +curl -X POST http://localhost:3000/api/ping \ + -H "Content-Type: application/json" \ + -d '{ + "target": "8.8.8.8", + "count": 4, + "timeout": 5000 + }' +``` + +**Respuesta:** +```json +{ + "success": true, + "target": "8.8.8.8", + "timestamp": "2025-08-16T21:00:00.000Z", + "results": [ + { + "sequence": 1, + "time": 15.2, + "alive": true, + "host": "8.8.8.8" + } + ], + "statistics": { + "packetsTransmitted": 4, + "packetsReceived": 4, + "packetLoss": "0.0", + "min": 14.1, + "max": 16.8, + "avg": "15.20" + } +} +``` + +### 2. Ping a Hostname + +```bash +curl -X POST http://localhost:3000/api/ping \ + -H "Content-Type: application/json" \ + -d '{ + "target": "google.com", + "count": 3, + "timeout": 3000 + }' +``` + +### 3. Ping Rápido (1 ping) + +```bash +curl -X POST http://localhost:3000/api/ping \ + -H "Content-Type: application/json" \ + -d '{ + "target": "1.1.1.1", + "count": 1, + "timeout": 2000 + }' +``` + +### 4. Verificar Estado del Servicio + +```bash +curl -X GET http://localhost:3000/api/status +``` + +## 🚨 Ejemplos de Errores + +### Rate Limit Excedido + +```bash +# Después de 10 requests en 1 minuto +curl -X POST http://localhost:3000/api/ping \ + -H "Content-Type: application/json" \ + -d '{"target": "8.8.8.8"}' +``` + +**Respuesta (HTTP 429):** +```json +{ + "error": "Rate limit exceeded", + "message": "Too many requests. Please try again later.", + "resetTime": 1692180660000, + "limit": 10, + "remaining": 0 +} +``` + +### IP Privada (Bloqueada) + +```bash +curl -X POST http://localhost:3000/api/ping \ + -H "Content-Type: application/json" \ + -d '{"target": "192.168.1.1"}' +``` + +**Respuesta (HTTP 400):** +```json +{ + "error": "Invalid target", + "message": "Private, loopback, or reserved IP addresses are not allowed" +} +``` + +### Target Inválido + +```bash +curl -X POST http://localhost:3000/api/ping \ + -H "Content-Type: application/json" \ + -d '{"target": "invalid..hostname"}' +``` + +**Respuesta (HTTP 400):** +```json +{ + "error": "Invalid target", + "message": "Invalid IP address or hostname format" +} +``` + +## 💻 Ejemplos en JavaScript + +### 1. Función de Ping Simple + +```javascript +async function pingHost(target, count = 4) { + try { + const response = await fetch('/api/ping', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + target, + count, + timeout: 5000 + }) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message); + } + + const result = await response.json(); + console.log(`Ping to ${target}:`); + console.log(`Average: ${result.statistics.avg}ms`); + console.log(`Packet Loss: ${result.statistics.packetLoss}%`); + + return result; + } catch (error) { + console.error('Ping failed:', error.message); + throw error; + } +} + +// Uso +pingHost('8.8.8.8', 3); +``` + +### 2. Monitor de Múltiples Hosts + +```javascript +async function monitorHosts(hosts) { + const results = []; + + for (const host of hosts) { + try { + const result = await pingHost(host, 2); + results.push({ + host, + success: true, + averageTime: result.statistics.avg, + packetLoss: result.statistics.packetLoss + }); + } catch (error) { + results.push({ + host, + success: false, + error: error.message + }); + } + + // Esperar 2 segundos entre hosts para no saturar + await new Promise(resolve => setTimeout(resolve, 2000)); + } + + return results; +} + +// Uso +const hosts = ['8.8.8.8', '1.1.1.1', 'google.com', 'github.com']; +monitorHosts(hosts).then(console.log); +``` + +### 3. Verificador de Rate Limit + +```javascript +async function checkRateLimit() { + try { + const response = await fetch('/api/status'); + const data = await response.json(); + + const rateLimit = data.clientInfo.rateLimit; + console.log(`Rate Limit: ${rateLimit.remaining}/${rateLimit.limit} remaining`); + + if (rateLimit.remaining < 3) { + console.warn('⚠️ Approaching rate limit!'); + } + + return rateLimit; + } catch (error) { + console.error('Failed to check rate limit:', error); + return null; + } +} +``` + +### 4. Ping con Reintentos + +```javascript +async function pingWithRetry(target, maxRetries = 3) { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + console.log(`Attempt ${attempt}/${maxRetries} for ${target}`); + + const result = await pingHost(target, 1); + console.log(`✅ Success on attempt ${attempt}`); + return result; + + } catch (error) { + console.log(`❌ Attempt ${attempt} failed: ${error.message}`); + + if (attempt === maxRetries) { + throw new Error(`Failed after ${maxRetries} attempts: ${error.message}`); + } + + // Esperar antes del siguiente intento + const delay = attempt * 1000; // 1s, 2s, 3s... + await new Promise(resolve => setTimeout(resolve, delay)); + } + } +} +``` + +## 🐍 Ejemplos en Python + +### 1. Cliente Python Simple + +```python +import requests +import json +import time + +class PingClient: + def __init__(self, base_url="http://localhost:3000"): + self.base_url = base_url + + def ping(self, target, count=4, timeout=5000): + """Realiza un ping al target especificado""" + url = f"{self.base_url}/api/ping" + data = { + "target": target, + "count": count, + "timeout": timeout + } + + try: + response = requests.post(url, json=data) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + print(f"Error: {e}") + return None + + def get_status(self): + """Obtiene el estado del servicio""" + url = f"{self.base_url}/api/status" + try: + response = requests.get(url) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + print(f"Error: {e}") + return None + +# Uso +client = PingClient() + +# Ping simple +result = client.ping("8.8.8.8", count=3) +if result: + print(f"Average: {result['statistics']['avg']}ms") + +# Verificar estado +status = client.get_status() +if status: + print(f"Service: {status['service']}") + print(f"Rate limit: {status['clientInfo']['rateLimit']['remaining']}/10") +``` + +### 2. Monitor Continuo + +```python +import time +import datetime + +def monitor_host(client, target, interval=60, duration=3600): + """Monitorea un host durante un período específico""" + start_time = time.time() + end_time = start_time + duration + + print(f"Monitoring {target} for {duration/60} minutes...") + + while time.time() < end_time: + timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + result = client.ping(target, count=1) + if result and result.get('success'): + avg_time = result['statistics']['avg'] + packet_loss = result['statistics']['packetLoss'] + print(f"[{timestamp}] {target}: {avg_time}ms (loss: {packet_loss}%)") + else: + print(f"[{timestamp}] {target}: FAILED") + + time.sleep(interval) + +# Uso +client = PingClient() +monitor_host(client, "8.8.8.8", interval=30, duration=1800) # 30 min +``` + +## 🔧 Headers de Rate Limiting + +El servicio incluye headers HTTP para monitorear el rate limiting: + +```bash +curl -I -X POST http://localhost:3000/api/ping \ + -H "Content-Type: application/json" \ + -d '{"target": "8.8.8.8"}' +``` + +**Headers de respuesta:** +``` +X-RateLimit-Limit: 10 +X-RateLimit-Remaining: 9 +X-RateLimit-Reset: 1692180660000 +``` + +## 🚀 Casos de Uso Avanzados + +### Dashboard de Monitoreo + +```javascript +class NetworkDashboard { + constructor() { + this.hosts = ['8.8.8.8', '1.1.1.1', 'google.com']; + this.results = new Map(); + } + + async updateStatus() { + for (const host of this.hosts) { + try { + const result = await pingHost(host, 1); + this.results.set(host, { + status: 'up', + latency: result.statistics.avg, + lastCheck: new Date() + }); + } catch (error) { + this.results.set(host, { + status: 'down', + error: error.message, + lastCheck: new Date() + }); + } + } + + this.displayResults(); + } + + displayResults() { + console.clear(); + console.log('🌐 Network Status Dashboard'); + console.log('=' .repeat(40)); + + this.results.forEach((result, host) => { + const icon = result.status === 'up' ? '✅' : '❌'; + const info = result.status === 'up' + ? `${result.latency}ms` + : result.error; + + console.log(`${icon} ${host.padEnd(15)} ${info}`); + }); + } + + start(interval = 30000) { + this.updateStatus(); + setInterval(() => this.updateStatus(), interval); + } +} + +// Uso +const dashboard = new NetworkDashboard(); +dashboard.start(30000); // Actualizar cada 30 segundos +``` + +Estos ejemplos muestran cómo integrar el servicio de ping en diferentes aplicaciones y escenarios de uso real. diff --git a/README.md b/README.md index 66bb426..65fc09b 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,61 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). +# API Ping Service -## Getting Started +Un servicio moderno de ping construido con Next.js 15 que permite realizar pruebas de conectividad de red de forma segura y controlada. -First, run the development server: +## 🚀 Características + +- **Rate Limiting**: Máximo 10 peticiones por minuto por IP para prevenir abuso +- **Validación de seguridad**: Bloquea IPs privadas, localhost y rangos reservados +- **Interfaz moderna**: UI responsiva con Tailwind CSS +- **Tiempo real**: Resultados en tiempo real con indicadores de progreso +- **Estadísticas completas**: Métricas detalladas de latencia, pérdida de paquetes, etc. +- **API RESTful**: Endpoints bien documentados para integración + +## 🔧 Instalación ```bash +# Instalar dependencias +npm install + +# Ejecutar en modo desarrollo npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev + +# Ejecutar en producción +npm run build +npm start ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +## 📡 API Endpoints -You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file. +### POST /api/ping +Realiza una prueba de ping al destino especificado. -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +**Request Body:** +```json +{ + "target": "8.8.8.8", // IP o hostname (requerido) + "count": 4, // Número de pings (1-10, default: 4) + "timeout": 5000 // Timeout en ms (1000-10000, default: 5000) +} +``` -## Learn More +### GET /api/status +Obtiene información del estado del servicio y estadísticas del cliente. -To learn more about Next.js, take a look at the following resources: +## 🛡️ Seguridad -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +### Rate Limiting +- **Límite**: 10 peticiones por minuto por IP +- **Ventana deslizante**: Se renueva automáticamente -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +### Validaciones +- ✅ IPs públicas válidas (IPv4/IPv6) +- ✅ Hostnames válidos según RFC 1123 +- ❌ IPs privadas, localhost y rangos reservados -## Deploy on Vercel +## 🎨 Interfaz de Usuario -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +- **Responsive**: Optimizada para móviles y escritorio +- **Dark Mode**: Soporte completo para tema oscuro +- **Tiempo Real**: Indicadores de progreso durante las pruebas +- **Quick Select**: Botones rápidos para destinos comunes diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 1e57433..0000000 --- a/package-lock.json +++ /dev/null @@ -1,1670 +0,0 @@ -{ - "name": "api-ping", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "api-ping", - "version": "0.1.0", - "dependencies": { - "next": "15.4.6", - "react": "19.1.0", - "react-dom": "19.1.0" - }, - "devDependencies": { - "@tailwindcss/postcss": "^4", - "tailwindcss": "^4" - } - }, - "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, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", - "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", - "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", - "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", - "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", - "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", - "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", - "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", - "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", - "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", - "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", - "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", - "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", - "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", - "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", - "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", - "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", - "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", - "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", - "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", - "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.4.4" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", - "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", - "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", - "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@next/env": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.4.6.tgz", - "integrity": "sha512-yHDKVTcHrZy/8TWhj0B23ylKv5ypocuCwey9ZqPyv4rPdUdRzpGCkSi03t04KBPyU96kxVtUqx6O3nE1kpxASQ==", - "license": "MIT" - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.6.tgz", - "integrity": "sha512-667R0RTP4DwxzmrqTs4Lr5dcEda9OxuZsVFsjVtxVMVhzSpo6nLclXejJVfQo2/g7/Z9qF3ETDmN3h65mTjpTQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.4.6.tgz", - "integrity": "sha512-KMSFoistFkaiQYVQQnaU9MPWtp/3m0kn2Xed1Ces5ll+ag1+rlac20sxG+MqhH2qYWX1O2GFOATQXEyxKiIscg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.4.6.tgz", - "integrity": "sha512-PnOx1YdO0W7m/HWFeYd2A6JtBO8O8Eb9h6nfJia2Dw1sRHoHpNf6lN1U4GKFRzRDBi9Nq2GrHk9PF3Vmwf7XVw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.4.6.tgz", - "integrity": "sha512-XBbuQddtY1p5FGPc2naMO0kqs4YYtLYK/8aPausI5lyOjr4J77KTG9mtlU4P3NwkLI1+OjsPzKVvSJdMs3cFaw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.4.6.tgz", - "integrity": "sha512-+WTeK7Qdw82ez3U9JgD+igBAP75gqZ1vbK6R8PlEEuY0OIe5FuYXA4aTjL811kWPf7hNeslD4hHK2WoM9W0IgA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.4.6.tgz", - "integrity": "sha512-XP824mCbgQsK20jlXKrUpZoh/iO3vUWhMpxCz8oYeagoiZ4V0TQiKy0ASji1KK6IAe3DYGfj5RfKP6+L2020OQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.4.6.tgz", - "integrity": "sha512-FxrsenhUz0LbgRkNWx6FRRJIPe/MI1JRA4W4EPd5leXO00AZ6YU8v5vfx4MDXTvN77lM/EqsE3+6d2CIeF5NYg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.4.6.tgz", - "integrity": "sha512-T4ufqnZ4u88ZheczkBTtOF+eKaM14V8kbjud/XrAakoM5DKQWjW09vD6B9fsdsWS2T7D5EY31hRHdta7QKWOng==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@tailwindcss/node": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.12.tgz", - "integrity": "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "enhanced-resolve": "^5.18.3", - "jiti": "^2.5.1", - "lightningcss": "1.30.1", - "magic-string": "^0.30.17", - "source-map-js": "^1.2.1", - "tailwindcss": "4.1.12" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.12.tgz", - "integrity": "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.4", - "tar": "^7.4.3" - }, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.12", - "@tailwindcss/oxide-darwin-arm64": "4.1.12", - "@tailwindcss/oxide-darwin-x64": "4.1.12", - "@tailwindcss/oxide-freebsd-x64": "4.1.12", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.12", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.12", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.12", - "@tailwindcss/oxide-linux-x64-musl": "4.1.12", - "@tailwindcss/oxide-wasm32-wasi": "4.1.12", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.12", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.12" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.12.tgz", - "integrity": "sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.12.tgz", - "integrity": "sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.12.tgz", - "integrity": "sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.12.tgz", - "integrity": "sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.12.tgz", - "integrity": "sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.12.tgz", - "integrity": "sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.12.tgz", - "integrity": "sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.12.tgz", - "integrity": "sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.12.tgz", - "integrity": "sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.12.tgz", - "integrity": "sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==", - "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.4.5", - "@emnapi/runtime": "^1.4.5", - "@emnapi/wasi-threads": "^1.0.4", - "@napi-rs/wasm-runtime": "^0.2.12", - "@tybys/wasm-util": "^0.10.0", - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz", - "integrity": "sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.12.tgz", - "integrity": "sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/postcss": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.12.tgz", - "integrity": "sha512-5PpLYhCAwf9SJEeIsSmCDLgyVfdBhdBpzX1OJ87anT9IVR0Z9pjM0FNixCAUAHGnMBGB8K99SwAheXrT0Kh6QQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.12", - "@tailwindcss/oxide": "4.1.12", - "postcss": "^8.4.41", - "tailwindcss": "4.1.12" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001735", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz", - "integrity": "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==", - "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/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" - }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "license": "MIT", - "optional": true, - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "optional": 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==", - "license": "MIT", - "optional": true - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "optional": true, - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "devOptional": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "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, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT", - "optional": true - }, - "node_modules/jiti": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", - "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/lightningcss": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", - "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-darwin-arm64": "1.30.1", - "lightningcss-darwin-x64": "1.30.1", - "lightningcss-freebsd-x64": "1.30.1", - "lightningcss-linux-arm-gnueabihf": "1.30.1", - "lightningcss-linux-arm64-gnu": "1.30.1", - "lightningcss-linux-arm64-musl": "1.30.1", - "lightningcss-linux-x64-gnu": "1.30.1", - "lightningcss-linux-x64-musl": "1.30.1", - "lightningcss-win32-arm64-msvc": "1.30.1", - "lightningcss-win32-x64-msvc": "1.30.1" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", - "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "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.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", - "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "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.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", - "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "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.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", - "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "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.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", - "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "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.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", - "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "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.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", - "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "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.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", - "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "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.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", - "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "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.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", - "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minizlib": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/next": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/next/-/next-15.4.6.tgz", - "integrity": "sha512-us++E/Q80/8+UekzB3SAGs71AlLDsadpFMXVNM/uQ0BMwsh9m3mr0UNQIfjKed8vpWXsASe+Qifrnu1oLIcKEQ==", - "license": "MIT", - "dependencies": { - "@next/env": "15.4.6", - "@swc/helpers": "0.5.15", - "caniuse-lite": "^1.0.30001579", - "postcss": "8.4.31", - "styled-jsx": "5.1.6" - }, - "bin": { - "next": "dist/bin/next" - }, - "engines": { - "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "15.4.6", - "@next/swc-darwin-x64": "15.4.6", - "@next/swc-linux-arm64-gnu": "15.4.6", - "@next/swc-linux-arm64-musl": "15.4.6", - "@next/swc-linux-x64-gnu": "15.4.6", - "@next/swc-linux-x64-musl": "15.4.6", - "@next/swc-win32-arm64-msvc": "15.4.6", - "@next/swc-win32-x64-msvc": "15.4.6", - "sharp": "^0.34.3" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.51.1", - "babel-plugin-react-compiler": "*", - "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@playwright/test": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/next/node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "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.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/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/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", - "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.26.0" - }, - "peerDependencies": { - "react": "^19.1.0" - } - }, - "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sharp": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", - "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.4", - "semver": "^7.7.2" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.3", - "@img/sharp-darwin-x64": "0.34.3", - "@img/sharp-libvips-darwin-arm64": "1.2.0", - "@img/sharp-libvips-darwin-x64": "1.2.0", - "@img/sharp-libvips-linux-arm": "1.2.0", - "@img/sharp-libvips-linux-arm64": "1.2.0", - "@img/sharp-libvips-linux-ppc64": "1.2.0", - "@img/sharp-libvips-linux-s390x": "1.2.0", - "@img/sharp-libvips-linux-x64": "1.2.0", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", - "@img/sharp-libvips-linuxmusl-x64": "1.2.0", - "@img/sharp-linux-arm": "0.34.3", - "@img/sharp-linux-arm64": "0.34.3", - "@img/sharp-linux-ppc64": "0.34.3", - "@img/sharp-linux-s390x": "0.34.3", - "@img/sharp-linux-x64": "0.34.3", - "@img/sharp-linuxmusl-arm64": "0.34.3", - "@img/sharp-linuxmusl-x64": "0.34.3", - "@img/sharp-wasm32": "0.34.3", - "@img/sharp-win32-arm64": "0.34.3", - "@img/sharp-win32-ia32": "0.34.3", - "@img/sharp-win32-x64": "0.34.3" - } - }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "license": "MIT", - "optional": true, - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/styled-jsx": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", - "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", - "license": "MIT", - "dependencies": { - "client-only": "0.0.1" - }, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/tailwindcss": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz", - "integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", - "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tar": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "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/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - } - } -} diff --git a/package.json b/package.json index fe3c45f..9dd8289 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,15 @@ "lint": "next lint" }, "dependencies": { + "@vercel/kv": "^3.0.0", + "ip-regex": "^5.0.0", + "next": "15.4.6", + "node-cache": "^5.1.2", + "ping": "^0.4.4", "react": "19.1.0", "react-dom": "19.1.0", - "next": "15.4.6" + "redis-memory-server": "^0.12.1", + "validator": "^13.15.15" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/src/app/api/ping/route.js b/src/app/api/ping/route.js new file mode 100644 index 0000000..84f2353 --- /dev/null +++ b/src/app/api/ping/route.js @@ -0,0 +1,166 @@ +import ping from 'ping'; +import { NextResponse } from 'next/server'; +import { RateLimiter } from '../../../lib/rate-limiter'; +import { validateIP } from '../../../lib/validators'; +import { PING_CONFIG } from '../../../lib/config.js'; + +// Crear instancia del rate limiter +const rateLimiter = new RateLimiter(); + +export async function POST(request) { + try { + // Obtener IP del cliente para rate limiting + const forwarded = request.headers.get('x-forwarded-for'); + const clientIP = forwarded + ? forwarded.split(',')[0].trim() + : request.headers.get('x-real-ip') || 'unknown'; + + // Verificar rate limit + const rateLimitResult = await rateLimiter.checkLimit(clientIP); + if (!rateLimitResult.allowed) { + return NextResponse.json({ + error: 'Rate limit exceeded', + message: 'Too many requests. Please try again later.', + resetTime: rateLimitResult.resetTime, + limit: rateLimitResult.limit, + remaining: rateLimitResult.remaining + }, { + status: 429, + headers: { + 'X-RateLimit-Limit': rateLimitResult.limit.toString(), + 'X-RateLimit-Remaining': rateLimitResult.remaining.toString(), + 'X-RateLimit-Reset': rateLimitResult.resetTime.toString() + } + }); + } + + // Parsear el body de la request + const body = await request.json(); + const { target, count = 4, timeout = 5000 } = body; + + // Validar parámetros + if (!target) { + return NextResponse.json({ + error: 'Missing target', + message: 'Target IP or hostname is required' + }, { status: 400 }); + } + + // Validar IP/hostname + const validationResult = validateIP(target); + if (!validationResult.valid) { + return NextResponse.json({ + error: 'Invalid target', + message: validationResult.message + }, { status: 400 }); + } + + // Validar count (usar configuración) + const pingCount = Math.min( + Math.max(parseInt(count) || PING_CONFIG.PING_LIMITS.DEFAULT_COUNT, PING_CONFIG.PING_LIMITS.MIN_COUNT), + PING_CONFIG.PING_LIMITS.MAX_COUNT + ); + + // Validar timeout (usar configuración) + const pingTimeout = Math.min( + Math.max(parseInt(timeout) || PING_CONFIG.PING_LIMITS.DEFAULT_TIMEOUT, PING_CONFIG.PING_LIMITS.MIN_TIMEOUT), + PING_CONFIG.PING_LIMITS.MAX_TIMEOUT + ); + + // Realizar el ping + const results = []; + const startTime = Date.now(); + + for (let i = 0; i < pingCount; i++) { + try { + const result = await ping.promise.probe(target, { + timeout: pingTimeout / 1000, // ping library expects seconds + min_reply: 1, + deadline: 30 + }); + + results.push({ + sequence: i + 1, + time: result.time === 'unknown' ? null : parseFloat(result.time), + alive: result.alive, + host: result.host, + numeric_host: result.numeric_host, + output: result.output + }); + + // Esperar entre pings (usar configuración) + if (i < pingCount - 1) { + await new Promise(resolve => setTimeout(resolve, PING_CONFIG.NETWORK.PING_INTERVAL_MS)); + } + } catch (error) { + results.push({ + sequence: i + 1, + time: null, + alive: false, + host: target, + error: error.message + }); + } + } + + const endTime = Date.now(); + const totalTime = endTime - startTime; + + // Calcular estadísticas + const successfulPings = results.filter(r => r.alive && r.time !== null); + const times = successfulPings.map(r => r.time); + + const stats = { + packetsTransmitted: pingCount, + packetsReceived: successfulPings.length, + packetLoss: ((pingCount - successfulPings.length) / pingCount * 100).toFixed(1), + totalTime: totalTime, + min: times.length > 0 ? Math.min(...times) : null, + max: times.length > 0 ? Math.max(...times) : null, + avg: times.length > 0 ? (times.reduce((a, b) => a + b, 0) / times.length).toFixed(2) : null + }; + + return NextResponse.json({ + success: true, + target: target, + timestamp: new Date().toISOString(), + results: results, + statistics: stats, + rateLimit: { + limit: rateLimitResult.limit, + remaining: rateLimitResult.remaining - 1, + resetTime: rateLimitResult.resetTime + } + }, { + headers: { + 'X-RateLimit-Limit': rateLimitResult.limit.toString(), + 'X-RateLimit-Remaining': (rateLimitResult.remaining - 1).toString(), + 'X-RateLimit-Reset': rateLimitResult.resetTime.toString() + } + }); + + } catch (error) { + console.error('Ping API error:', error); + + return NextResponse.json({ + error: 'Internal server error', + message: 'An error occurred while processing the ping request' + }, { status: 500 }); + } +} + +export async function GET() { + return NextResponse.json({ + message: 'Ping API Service', + description: 'Send a POST request with a target IP or hostname to perform a ping test', + usage: { + method: 'POST', + body: { + target: 'IP address or hostname (required)', + count: 'Number of pings (1-10, default: 4)', + timeout: 'Timeout in milliseconds (1000-10000, default: 5000)' + } + }, + rateLimit: 'Maximum 10 requests per minute per IP address' + }); +} diff --git a/src/app/api/status/route.js b/src/app/api/status/route.js new file mode 100644 index 0000000..914919a --- /dev/null +++ b/src/app/api/status/route.js @@ -0,0 +1,69 @@ +import { NextResponse } from 'next/server'; +import { RateLimiter } from '../../../lib/rate-limiter'; + +// Crear instancia del rate limiter +const rateLimiter = new RateLimiter(); + +export async function GET(request) { + try { + // Obtener IP del cliente + const forwarded = request.headers.get('x-forwarded-for'); + const clientIP = forwarded + ? forwarded.split(',')[0].trim() + : request.headers.get('x-real-ip') || 'unknown'; + + // Obtener estadísticas del rate limiter para este cliente + const rateLimitStats = rateLimiter.getStats(clientIP); + + return NextResponse.json({ + service: 'API Ping Service', + version: '1.0.0', + status: 'operational', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + clientInfo: { + ip: clientIP, + rateLimit: rateLimitStats + }, + features: { + maxPingsPerRequest: 10, + maxTimeout: 10000, + minTimeout: 1000, + supportedProtocols: ['ICMP'], + restrictions: [ + 'No private IP addresses', + 'No localhost addresses', + 'No reserved IP ranges', + 'Rate limited to 10 requests per minute' + ] + }, + endpoints: { + ping: { + path: '/api/ping', + method: 'POST', + description: 'Perform ping test to specified target', + parameters: { + target: 'IP address or hostname (required)', + count: 'Number of pings (1-10, default: 4)', + timeout: 'Timeout in milliseconds (1000-10000, default: 5000)' + } + }, + status: { + path: '/api/status', + method: 'GET', + description: 'Get service status and client information' + } + } + }); + + } catch (error) { + console.error('Status API error:', error); + + return NextResponse.json({ + service: 'API Ping Service', + status: 'error', + timestamp: new Date().toISOString(), + error: 'Internal server error' + }, { status: 500 }); + } +} diff --git a/src/app/components/CopyButton.js b/src/app/components/CopyButton.js new file mode 100644 index 0000000..7cc0f6f --- /dev/null +++ b/src/app/components/CopyButton.js @@ -0,0 +1,45 @@ +'use client'; + +import { useState } from 'react'; + +export default function CopyButton({ text, className = '' }) { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error('Failed to copy text: ', err); + } + }; + + return ( + + ); +} diff --git a/src/app/components/PingForm.js b/src/app/components/PingForm.js new file mode 100644 index 0000000..360ce77 --- /dev/null +++ b/src/app/components/PingForm.js @@ -0,0 +1,137 @@ +'use client'; + +import { useState } from 'react'; + +export default function PingForm({ onSubmit, isLoading, onCancel }) { + const [target, setTarget] = useState(''); + const [count, setCount] = useState(4); + const [timeout, setTimeout] = useState(5000); + + const handleSubmit = (e) => { + e.preventDefault(); + if (!target.trim()) return; + + onSubmit({ + target: target.trim(), + count: parseInt(count), + timeout: parseInt(timeout), + }); + }; + + const commonTargets = [ + { name: 'Google DNS', value: '8.8.8.8' }, + { name: 'Cloudflare DNS', value: '1.1.1.1' }, + { name: 'Google.com', value: 'google.com' }, + { name: 'GitHub.com', value: 'github.com' }, + ]; + + return ( +
+
+ + setTarget(e.target.value)} + placeholder="e.g., 8.8.8.8 or google.com" + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" + required + disabled={isLoading} + /> + + {/* Quick Select Buttons */} +
+ Quick select: + {commonTargets.map((item) => ( + + ))} +
+
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + + {isLoading && onCancel && ( + + )} +
+
+ ); +} diff --git a/src/app/components/PingInterface.js b/src/app/components/PingInterface.js new file mode 100644 index 0000000..088ee09 --- /dev/null +++ b/src/app/components/PingInterface.js @@ -0,0 +1,111 @@ +'use client'; + +import { useEffect } from 'react'; +import { usePingService } from '../../hooks/usePingService'; +import PingForm from './PingForm'; +import PingResults from './PingResults'; +import RateLimitInfo from './RateLimitInfo'; + +export default function PingInterface() { + const { + isLoading, + results, + error, + rateLimitInfo, + executePing, + cancelPing, + clearResults, + fetchStatus + } = usePingService(); + + // Cargar información inicial del estado al montar el componente + useEffect(() => { + fetchStatus(); + }, [fetchStatus]); + + return ( +
+ {/* Rate Limit Information */} + {rateLimitInfo && } + + {/* Ping Form */} +
+ +
+ + {/* Error Display */} + {error && ( +
+
+
+
+ + + +
+
+

+ Error +

+
+ {error} +
+
+
+ +
+
+ )} + + {/* Results Display */} + {results && ( +
+ +
+ +
+
+ )} + + {/* Usage Information */} +
+
+
+ + + +
+
+

+ Usage Guidelines +

+
    +
  • • Maximum 10 requests per minute per IP address
  • +
  • • Maximum 10 pings per request
  • +
  • • Timeout range: 1-10 seconds
  • +
  • • Private IPs and localhost are blocked for security
  • +
  • • Only public IP addresses and valid hostnames are allowed
  • +
+
+
+
+
+ ); +} diff --git a/src/app/components/PingResults.js b/src/app/components/PingResults.js new file mode 100644 index 0000000..8daa165 --- /dev/null +++ b/src/app/components/PingResults.js @@ -0,0 +1,176 @@ +'use client'; + +import CopyButton from './CopyButton'; + +export default function PingResults({ results }) { + const { target, timestamp, results: pingResults, statistics } = results; + + const formatTime = (time) => { + if (time === null || time === undefined) return 'N/A'; + return `${time}ms`; + }; + + const formatTimestamp = (isoString) => { + return new Date(isoString).toLocaleString(); + }; + + const getStatusColor = (alive) => { + return alive ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'; + }; + + const getStatusIcon = (alive) => { + if (alive) { + return ( + + + + ); + } else { + return ( + + + + ); + } + }; + + return ( +
+ {/* Header */} +
+

+ Ping Results for {target} +

+

+ Test completed at {formatTimestamp(timestamp)} +

+
+ + {/* Statistics Summary */} +
+
+
+
+ {statistics.packetsTransmitted} +
+
Sent
+
+
+
+ {statistics.packetsReceived} +
+
Received
+
+
+
0 ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400'}`}> + {statistics.packetLoss}% +
+
Loss
+
+
+
+ {statistics.avg ? `${statistics.avg}ms` : 'N/A'} +
+
Avg Time
+
+
+ + {statistics.min !== null && statistics.max !== null && ( +
+ Min: {statistics.min}ms • Max: {statistics.max}ms • Total Time: {statistics.totalTime}ms +
+ )} +
+ + {/* Individual Ping Results */} +
+

+ Individual Ping Results +

+
+ {pingResults.map((result) => ( +
+
+ {getStatusIcon(result.alive)} + + Ping #{result.sequence} + + {result.host && result.host !== target && ( + + ({result.host}) + + )} +
+ +
+ + {result.alive ? 'Success' : 'Failed'} + + + {formatTime(result.time)} + +
+
+ ))} +
+
+ + {/* Raw Output (if available) */} + {pingResults.some(r => r.output) && ( +
+
+ +
+ + + + Show Raw Terminal Output + + {pingResults.filter(r => r.output).length} ping{pingResults.filter(r => r.output).length !== 1 ? 's' : ''} + +
+ r.output).map(r => `Ping #${r.sequence}:\n${r.output}`).join('\n\n')} + className="ml-2" + /> +
+
+ {pingResults + .filter(r => r.output) + .map((r, index) => ( +
+
+
+
+
+
+
+
+ + Ping #{r.sequence} - {r.alive ? '✅ Success' : '❌ Failed'} + +
+
+ + {r.time ? `${r.time}ms` : 'No response'} + + +
+
+
+
+                        {r.output}
+                      
+
+
+ ))} +
+
+
+ )} +
+ ); +} diff --git a/src/app/components/RateLimitInfo.js b/src/app/components/RateLimitInfo.js new file mode 100644 index 0000000..29f0549 --- /dev/null +++ b/src/app/components/RateLimitInfo.js @@ -0,0 +1,116 @@ +'use client'; + +export default function RateLimitInfo({ rateLimitInfo }) { + const { limit, remaining, resetTime } = rateLimitInfo; + + const formatResetTime = (timestamp) => { + const resetDate = new Date(timestamp); + const now = new Date(); + const diffMs = resetDate - now; + + if (diffMs <= 0) { + return 'Now'; + } + + const diffSeconds = Math.ceil(diffMs / 1000); + + if (diffSeconds < 60) { + return `${diffSeconds}s`; + } + + const diffMinutes = Math.ceil(diffSeconds / 60); + return `${diffMinutes}m`; + }; + + const getStatusColor = () => { + const percentage = (remaining / limit) * 100; + + if (percentage > 50) { + return 'bg-green-500'; + } else if (percentage > 20) { + return 'bg-yellow-500'; + } else { + return 'bg-red-500'; + } + }; + + const getTextColor = () => { + const percentage = (remaining / limit) * 100; + + if (percentage > 50) { + return 'text-green-700 dark:text-green-300'; + } else if (percentage > 20) { + return 'text-yellow-700 dark:text-yellow-300'; + } else { + return 'text-red-700 dark:text-red-300'; + } + }; + + const getBorderColor = () => { + const percentage = (remaining / limit) * 100; + + if (percentage > 50) { + return 'border-green-200 dark:border-green-800'; + } else if (percentage > 20) { + return 'border-yellow-200 dark:border-yellow-800'; + } else { + return 'border-red-200 dark:border-red-800'; + } + }; + + const getBgColor = () => { + const percentage = (remaining / limit) * 100; + + if (percentage > 50) { + return 'bg-green-50 dark:bg-green-900/20'; + } else if (percentage > 20) { + return 'bg-yellow-50 dark:bg-yellow-900/20'; + } else { + return 'bg-red-50 dark:bg-red-900/20'; + } + }; + + return ( +
+
+

+ Rate Limit Status +

+ + Resets in {formatResetTime(resetTime)} + +
+ +
+
+
+ Requests remaining + {remaining} / {limit} +
+ +
+
+
+
+ +
+
+ {remaining} +
+
+ left +
+
+
+ + {remaining <= 2 && ( +
+ ⚠️ You're approaching the rate limit. Please wait before making more requests. +
+ )} +
+ ); +} diff --git a/src/app/page.js b/src/app/page.js index d0d0a6a..b9070c0 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -1,103 +1,27 @@ -import Image from "next/image"; +import PingInterface from './components/PingInterface'; export default function Home() { return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - src/app/page.js - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
+
+
+
+

+ 🏓 API Ping Service +

+

+ Test network connectivity to any IP address or hostname with our secure ping service. + Rate limited for fair usage and designed with modern security practices. +

+
+ +
+ +
- -
- + +
); } diff --git a/src/hooks/usePingService.js b/src/hooks/usePingService.js new file mode 100644 index 0000000..85c8492 --- /dev/null +++ b/src/hooks/usePingService.js @@ -0,0 +1,108 @@ +import { useState, useCallback, useRef } from 'react'; + +export function usePingService() { + const [isLoading, setIsLoading] = useState(false); + const [results, setResults] = useState(null); + const [error, setError] = useState(null); + const [rateLimitInfo, setRateLimitInfo] = useState(null); + const abortControllerRef = useRef(null); + + const executePing = useCallback(async (formData) => { + // Cancelar request anterior si existe + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + // Crear nuevo AbortController + abortControllerRef.current = new AbortController(); + + setIsLoading(true); + setError(null); + setResults(null); + + try { + const response = await fetch('/api/ping', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(formData), + signal: abortControllerRef.current.signal, + }); + + const data = await response.json(); + + // Actualizar información de rate limit desde headers + if (response.headers.get('X-RateLimit-Limit')) { + setRateLimitInfo({ + limit: parseInt(response.headers.get('X-RateLimit-Limit')), + remaining: parseInt(response.headers.get('X-RateLimit-Remaining')), + resetTime: parseInt(response.headers.get('X-RateLimit-Reset')), + }); + } + + if (!response.ok) { + throw new Error(data.message || `HTTP ${response.status}: ${response.statusText}`); + } + + setResults(data); + + // Actualizar rate limit info desde la respuesta si está disponible + if (data.rateLimit) { + setRateLimitInfo(data.rateLimit); + } + + } catch (err) { + // No mostrar error si fue cancelado + if (err.name !== 'AbortError') { + setError(err.message); + } + } finally { + setIsLoading(false); + abortControllerRef.current = null; + } + }, []); + + const cancelPing = useCallback(() => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + abortControllerRef.current = null; + setIsLoading(false); + } + }, []); + + const clearResults = useCallback(() => { + setResults(null); + setError(null); + }, []); + + const fetchStatus = useCallback(async () => { + try { + const response = await fetch('/api/status'); + const data = await response.json(); + + if (data.clientInfo?.rateLimit) { + setRateLimitInfo(data.clientInfo.rateLimit); + } + + return data; + } catch (err) { + console.error('Failed to fetch status:', err); + return null; + } + }, []); + + return { + // Estado + isLoading, + results, + error, + rateLimitInfo, + + // Acciones + executePing, + cancelPing, + clearResults, + fetchStatus, + }; +} diff --git a/src/lib/config.js b/src/lib/config.js new file mode 100644 index 0000000..8fa09fc --- /dev/null +++ b/src/lib/config.js @@ -0,0 +1,119 @@ +// Configuración del servicio de ping +export const PING_CONFIG = { + // Rate limiting + RATE_LIMIT: { + MAX_REQUESTS: parseInt(process.env.RATE_LIMIT_MAX) || 10, + WINDOW_MS: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 60 * 1000, // 1 minuto + }, + + // Límites de ping + PING_LIMITS: { + MAX_COUNT: parseInt(process.env.PING_MAX_COUNT) || 10, + MIN_COUNT: 1, + MAX_TIMEOUT: parseInt(process.env.PING_MAX_TIMEOUT) || 10000, // 10 segundos + MIN_TIMEOUT: 1000, // 1 segundo + DEFAULT_COUNT: 4, + DEFAULT_TIMEOUT: 5000, // 5 segundos + }, + + // Configuración de seguridad + SECURITY: { + BLOCK_PRIVATE_IPS: true, + BLOCK_LOCALHOST: true, + BLOCK_RESERVED_RANGES: true, + ALLOWED_PROTOCOLS: ['ICMP'], + }, + + // Configuración de red + NETWORK: { + MAX_CONCURRENT_PINGS: 5, + PING_INTERVAL_MS: 1000, // Intervalo entre pings individuales + MAX_HOSTNAME_LENGTH: 253, + MAX_LABEL_LENGTH: 63, + }, + + // Headers de respuesta + HEADERS: { + INCLUDE_RATE_LIMIT_HEADERS: true, + INCLUDE_SECURITY_HEADERS: true, + CORS_ENABLED: true, + }, + + // Logging y debugging + DEBUG: { + LOG_REQUESTS: process.env.NODE_ENV === 'development', + LOG_RATE_LIMITS: process.env.NODE_ENV === 'development', + INCLUDE_RAW_OUTPUT: true, + } +}; + +// Validador de configuración +export function validateConfig() { + const errors = []; + + if (PING_CONFIG.RATE_LIMIT.MAX_REQUESTS <= 0) { + errors.push('MAX_REQUESTS must be greater than 0'); + } + + if (PING_CONFIG.PING_LIMITS.MAX_COUNT > 50) { + errors.push('MAX_COUNT should not exceed 50 for performance reasons'); + } + + if (PING_CONFIG.PING_LIMITS.MAX_TIMEOUT > 30000) { + errors.push('MAX_TIMEOUT should not exceed 30 seconds'); + } + + if (errors.length > 0) { + throw new Error(`Configuration validation failed: ${errors.join(', ')}`); + } + + return true; +} + +// Configuración específica por entorno +export function getEnvironmentConfig() { + const env = process.env.NODE_ENV || 'development'; + + const envConfigs = { + development: { + RATE_LIMIT: { + MAX_REQUESTS: 20, // Más permisivo en desarrollo + WINDOW_MS: 60 * 1000, + }, + DEBUG: { + LOG_REQUESTS: true, + LOG_RATE_LIMITS: true, + INCLUDE_RAW_OUTPUT: true, + } + }, + + production: { + RATE_LIMIT: { + MAX_REQUESTS: 10, // Más restrictivo en producción + WINDOW_MS: 60 * 1000, + }, + DEBUG: { + LOG_REQUESTS: false, + LOG_RATE_LIMITS: false, + INCLUDE_RAW_OUTPUT: false, + } + }, + + test: { + RATE_LIMIT: { + MAX_REQUESTS: 100, // Sin límites en tests + WINDOW_MS: 60 * 1000, + }, + DEBUG: { + LOG_REQUESTS: false, + LOG_RATE_LIMITS: false, + INCLUDE_RAW_OUTPUT: false, + } + } + }; + + return { + ...PING_CONFIG, + ...envConfigs[env] + }; +} diff --git a/src/lib/rate-limiter.js b/src/lib/rate-limiter.js new file mode 100644 index 0000000..5726b50 --- /dev/null +++ b/src/lib/rate-limiter.js @@ -0,0 +1,100 @@ +import NodeCache from 'node-cache'; +import { PING_CONFIG } from './config.js'; + +export class RateLimiter { + constructor(options = {}) { + // Usar configuración por defecto o personalizada + this.limit = options.limit || PING_CONFIG.RATE_LIMIT.MAX_REQUESTS; + this.windowMs = options.windowMs || PING_CONFIG.RATE_LIMIT.WINDOW_MS; + + // Cache con TTL automático + this.cache = new NodeCache({ + stdTTL: this.windowMs / 1000, + checkperiod: 60 // revisar cada 60 segundos + }); + } + + async checkLimit(identifier) { + const key = `rate_limit:${identifier}`; + const now = Date.now(); + const windowStart = now - this.windowMs; + + // Obtener datos actuales del cache + let data = this.cache.get(key) || { + requests: [], + resetTime: now + this.windowMs + }; + + // Filtrar requests dentro de la ventana de tiempo + data.requests = data.requests.filter(timestamp => timestamp > windowStart); + + // Verificar si se excede el límite + const requestCount = data.requests.length; + const allowed = requestCount < this.limit; + + if (allowed) { + // Agregar la nueva request + data.requests.push(now); + + // Si es la primera request en esta ventana, actualizar resetTime + if (data.requests.length === 1) { + data.resetTime = now + this.windowMs; + } + + // Guardar en cache + this.cache.set(key, data, this.windowMs / 1000); + } + + return { + allowed, + limit: this.limit, + remaining: Math.max(0, this.limit - requestCount - (allowed ? 1 : 0)), + resetTime: data.resetTime, + requestCount: requestCount + (allowed ? 1 : 0) + }; + } + + // Método para obtener estadísticas de un cliente + getStats(identifier) { + const key = `rate_limit:${identifier}`; + const data = this.cache.get(key); + + if (!data) { + return { + limit: this.limit, + remaining: this.limit, + requestCount: 0, + resetTime: Date.now() + this.windowMs + }; + } + + const now = Date.now(); + const windowStart = now - this.windowMs; + const validRequests = data.requests.filter(timestamp => timestamp > windowStart); + + return { + limit: this.limit, + remaining: Math.max(0, this.limit - validRequests.length), + requestCount: validRequests.length, + resetTime: data.resetTime + }; + } + + // Método para limpiar el cache manualmente + clear() { + this.cache.flushAll(); + } + + // Método para obtener todas las estadísticas (útil para debugging) + getAllStats() { + const keys = this.cache.keys(); + const stats = {}; + + keys.forEach(key => { + const identifier = key.replace('rate_limit:', ''); + stats[identifier] = this.getStats(identifier); + }); + + return stats; + } +} diff --git a/src/lib/validators.js b/src/lib/validators.js new file mode 100644 index 0000000..147c5b0 --- /dev/null +++ b/src/lib/validators.js @@ -0,0 +1,149 @@ +import ipRegex from 'ip-regex'; + +export function validateIP(target) { + if (!target || typeof target !== 'string') { + return { + valid: false, + message: 'Target must be a valid string' + }; + } + + // Limpiar espacios en blanco + const cleanTarget = target.trim(); + + if (cleanTarget.length === 0) { + return { + valid: false, + message: 'Target cannot be empty' + }; + } + + // Verificar si es una IP válida (IPv4 o IPv6) + if (ipRegex().test(cleanTarget)) { + // Validaciones adicionales para IPs + if (isPrivateOrReservedIP(cleanTarget)) { + return { + valid: false, + message: 'Private, loopback, or reserved IP addresses are not allowed' + }; + } + + return { + valid: true, + type: 'ip', + message: 'Valid IP address' + }; + } + + // Si no es IP, verificar si es un hostname válido + if (isValidHostname(cleanTarget)) { + // Verificar que no sea localhost o dominios locales + if (isLocalHostname(cleanTarget)) { + return { + valid: false, + message: 'Local hostnames are not allowed' + }; + } + + return { + valid: true, + type: 'hostname', + message: 'Valid hostname' + }; + } + + return { + valid: false, + message: 'Invalid IP address or hostname format' + }; +} + +function isPrivateOrReservedIP(ip) { + // Rangos de IPs privadas y reservadas que no deberíamos permitir + const privateRanges = [ + /^127\./, // Loopback + /^10\./, // Private class A + /^172\.(1[6-9]|2[0-9]|3[01])\./, // Private class B + /^192\.168\./, // Private class C + /^169\.254\./, // Link-local + /^224\./, // Multicast + /^0\./, // Reserved + /^255\./, // Broadcast + /^::1$/, // IPv6 loopback + /^fe80:/, // IPv6 link-local + /^fc00:/, // IPv6 private + /^fd00:/ // IPv6 private + ]; + + return privateRanges.some(range => range.test(ip)); +} + +function isValidHostname(hostname) { + // Verificar longitud + if (hostname.length > 253) { + return false; + } + + // Patrón para hostname válido (RFC 1123) + const hostnameRegex = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i; + + if (!hostnameRegex.test(hostname)) { + return false; + } + + // Verificar que no termine con punto + if (hostname.endsWith('.')) { + return false; + } + + // Verificar que cada parte del hostname no sea demasiado larga + const parts = hostname.split('.'); + return parts.every(part => part.length <= 63 && part.length > 0); +} + +function isLocalHostname(hostname) { + const localHostnames = [ + 'localhost', + 'localhost.localdomain', + 'local', + 'internal', + 'intranet' + ]; + + const lowerHostname = hostname.toLowerCase(); + + // Verificar hostnames exactos + if (localHostnames.includes(lowerHostname)) { + return true; + } + + // Verificar dominios .local + if (lowerHostname.endsWith('.local')) { + return true; + } + + // Verificar si termina en .localhost + if (lowerHostname.endsWith('.localhost')) { + return true; + } + + return false; +} + +export function sanitizeTarget(target) { + if (typeof target !== 'string') { + return ''; + } + + return target.trim().toLowerCase(); +} + +export function isValidTimeout(timeout) { + const timeoutNum = parseInt(timeout); + return !isNaN(timeoutNum) && timeoutNum >= 1000 && timeoutNum <= 30000; +} + +export function isValidCount(count) { + const countNum = parseInt(count); + return !isNaN(countNum) && countNum >= 1 && countNum <= 10; +} diff --git a/src/middleware.js b/src/middleware.js new file mode 100644 index 0000000..71664aa --- /dev/null +++ b/src/middleware.js @@ -0,0 +1,38 @@ +import { NextResponse } from 'next/server'; + +export function middleware(request) { + // Crear la respuesta + const response = NextResponse.next(); + + // Agregar headers de seguridad + response.headers.set('X-Content-Type-Options', 'nosniff'); + response.headers.set('X-Frame-Options', 'DENY'); + response.headers.set('X-XSS-Protection', '1; mode=block'); + response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin'); + + // Headers específicos para API + if (request.nextUrl.pathname.startsWith('/api/')) { + response.headers.set('Access-Control-Allow-Origin', '*'); + response.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + response.headers.set('Access-Control-Allow-Headers', 'Content-Type'); + + // Manejar preflight requests + if (request.method === 'OPTIONS') { + return new Response(null, { status: 200, headers: response.headers }); + } + } + + return response; +} + +export const config = { + matcher: [ + /* + * Match all request paths except for the ones starting with: + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + */ + '/((?!_next/static|_next/image|favicon.ico).*)', + ], +};