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 (
+
+ {copied ? (
+ <>
+
+
+
+ Copied!
+ >
+ ) : (
+ <>
+
+
+
+ Copy
+ >
+ )}
+
+ );
+}
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 (
+
+ );
+}
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 && (
+
+
+
+
+ Clear 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'}
+
+
+
+
+
+
+ ))}
+
+
+
+ )}
+
+ );
+}
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 (
-
-
-
-
-
- Get started by editing{" "}
-
- src/app/page.js
-
- .
-
-
- Save and see your changes instantly.
-
-
+
);
}
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).*)',
+ ],
+};