From afb0d434bf983369ddf7c3412bd5b58ebc70b66d Mon Sep 17 00:00:00 2001 From: meninjar Date: Wed, 5 Aug 2026 01:30:50 +0000 Subject: [PATCH] first commit --- .env.example | 10 + .gitignore | 19 + Dockerfile | 31 + Dockerfile.prod | 45 + Makefile | 56 + docker-compose.yml | 53 + forensic_viewer-php.bk | 1493 +++++++++++ index.html | 16 + package-lock.json | 3667 ++++++++++++++++++++++++++++ package.json | 42 + server/index.js | 687 ++++++ server/logParser.js | 380 +++ src/App.tsx | 383 +++ src/components/BackupArchives.tsx | 71 + src/components/BruteForcePanel.tsx | 65 + src/components/FilterToolbar.tsx | 158 ++ src/components/LogTable.tsx | 320 +++ src/components/LoginModal.tsx | 186 ++ src/components/LoginScreen.tsx | 266 ++ src/components/Navbar.tsx | 71 + src/components/StatsOverview.tsx | 92 + src/index.css | 51 + src/main.tsx | 10 + src/types.ts | 64 + tsconfig.json | 24 + vite.config.ts | 23 + 26 files changed, 8283 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Dockerfile.prod create mode 100644 Makefile create mode 100644 docker-compose.yml create mode 100644 forensic_viewer-php.bk create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 server/index.js create mode 100644 server/logParser.js create mode 100644 src/App.tsx create mode 100644 src/components/BackupArchives.tsx create mode 100644 src/components/BruteForcePanel.tsx create mode 100644 src/components/FilterToolbar.tsx create mode 100644 src/components/LogTable.tsx create mode 100644 src/components/LoginModal.tsx create mode 100644 src/components/LoginScreen.tsx create mode 100644 src/components/Navbar.tsx create mode 100644 src/components/StatsOverview.tsx create mode 100644 src/index.css create mode 100644 src/main.tsx create mode 100644 src/types.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..77acd70 --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +PORT=5880 +LOGS_DIR=./logs +KEYCLOAK_URL=https://auth.rssa.top/ +KEYCLOAK_REALM=rssa +KEYCLOAK_CLIENT_ID=satu +KEYCLOAK_CLIENT_SECRET=ZhkK45MHB0a0eAZX5ecNTnlfnWlZXfBE +# URL Aplikasi SIMRS Utama (Kembali ke SIMRS Utama) +SIMRS_URL=http://meninjar.dev.rssa.id:4003 +# Opsional: Jika Keycloak membatasi redirect_uri ke port tertentu (misal 4003 / tanpa port) +KEYCLOAK_REDIRECT_URI= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dab1d8f --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# Dependencies +node_modules/ + +# Production build +dist/ + +# Logs +logs/ +*.log + +# Environment variables +.env +.env.local + +# Core dump files & OS junk +core +core.* +.DS_Store +Thumbs.db diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d8ef656 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +# ============================================ +# SIMRS Log Forensic Inspector — DEVELOPMENT +# ============================================ +# Mode: Hot-reload server via --watch-path +# Source: Mounted dari host via docker-compose volume +# Frontend: Disajikan dari dist/ yang di-mount dari host +# ============================================ + +FROM node:23-alpine + +WORKDIR /app + +# Copy dependency manifest first (cache layer) +COPY package*.json ./ + +# Install ALL dependencies (termasuk devDependencies) +RUN npm install + +# Copy source (akan di-override oleh volume mount saat dev) +COPY . . + +# Build frontend bundle awal +RUN npm run build + +EXPOSE 5880 + +ENV NODE_ENV=development +ENV PORT=5880 + +# Watch hanya folder server/ agar perubahan log tidak trigger restart +CMD ["node", "--watch-path=server", "server/index.js"] diff --git a/Dockerfile.prod b/Dockerfile.prod new file mode 100644 index 0000000..77e729c --- /dev/null +++ b/Dockerfile.prod @@ -0,0 +1,45 @@ +# ============================================ +# SIMRS Log Forensic Inspector — PRODUCTION +# ============================================ +# Mode: Multi-stage build, optimized image +# Frontend: Static bundle baked into image +# Server: Node.js tanpa devDependencies +# ============================================ + +# --- Stage 1: Build frontend --- +FROM node:23-alpine AS builder + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY . . +RUN npm run build + +# --- Stage 2: Production runtime --- +FROM node:23-alpine AS production + +WORKDIR /app + +# Install production dependencies only +COPY package*.json ./ +RUN npm ci --omit=dev && npm cache clean --force + +# Copy built frontend bundle +COPY --from=builder /app/dist ./dist + +# Copy server source code only +COPY server ./server + +EXPOSE 5880 + +ENV NODE_ENV=production +ENV PORT=5880 + +# Healthcheck: cek endpoint utama +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget --quiet --tries=1 --spider http://localhost:5880/ || exit 1 + +# Jalankan server langsung tanpa --watch +CMD ["node", "server/index.js"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9149568 --- /dev/null +++ b/Makefile @@ -0,0 +1,56 @@ +.PHONY: help dev-up dev-build prod-up prod-build down logs restart status clean + +APP_NAME = simrs-log-forensic +PORT = 5880 + +help: + @echo "" + @echo "╔══════════════════════════════════════════════════╗" + @echo "║ SIMRS Forensic Inspector (Port $(PORT)) ║" + @echo "╠══════════════════════════════════════════════════╣" + @echo "║ DEVELOPMENT ║" + @echo "║ make dev-up → Jalankan mode dev ║" + @echo "║ make dev-build → Build ulang image dev ║" + @echo "║ ║" + @echo "║ PRODUCTION ║" + @echo "║ make prod-up → Jalankan mode production ║" + @echo "║ make prod-build → Build ulang image prod ║" + @echo "║ ║" + @echo "║ UMUM ║" + @echo "║ make down → Hentikan & hapus ║" + @echo "║ make logs → Lihat log real-time ║" + @echo "║ make restart → Restart container ║" + @echo "║ make status → Cek status container ║" + @echo "║ make clean → Hapus node_modules & dist ║" + @echo "╚══════════════════════════════════════════════════╝" + @echo "" + +# ─── Development ────────────────────────────── +dev-up: + docker compose --profile dev up -d + +dev-build: + docker compose --profile dev up --build --force-recreate -d + +# ─── Production ─────────────────────────────── +prod-up: + docker compose --profile prod up -d + +prod-build: + docker compose --profile prod up --build --force-recreate -d + +# ─── Umum ───────────────────────────────────── +down: + docker compose --profile dev --profile prod down -v + +logs: + docker compose --profile dev --profile prod logs -f + +restart: + docker compose --profile dev --profile prod restart + +status: + docker compose --profile dev --profile prod ps + +clean: + rm -rf node_modules dist diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..aeaf1fe --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,53 @@ +# ============================================ +# SIMRS Log Forensic Inspector +# docker-compose.yml +# ============================================ +# Gunakan: +# make dev-up → Jalankan mode DEVELOPMENT +# make prod-up → Jalankan mode PRODUCTION +# ============================================ + +x-common: &common + restart: unless-stopped + ports: + - "5880:5880" + environment: &common-env + PORT: 5880 + LOGS_DIR: /app/logs + KEYCLOAK_URL: https://auth.rssa.top/ + KEYCLOAK_REALM: rssa + KEYCLOAK_CLIENT_ID: satu + KEYCLOAK_CLIENT_SECRET: ZhkK45MHB0a0eAZX5ecNTnlfnWlZXfBE + SIMRS_URL: http://meninjar.dev.rssa.id:4003 + +services: + # ─── Development ─────────────────────────── + dev: + <<: *common + container_name: simrs-log-forensic-dev + build: + context: . + dockerfile: Dockerfile + profiles: ["dev"] + environment: + <<: *common-env + NODE_ENV: development + NODE_OPTIONS: "--max-old-space-size=4096" + volumes: + - .:/app + - /app/node_modules + - /mnt/simrs/logs:/app/logs + + # ─── Production ──────────────────────────── + prod: + <<: *common + container_name: simrs-log-forensic + build: + context: . + dockerfile: Dockerfile.prod + profiles: ["prod"] + environment: + <<: *common-env + NODE_ENV: production + volumes: + - /mnt/simrs/logs:/app/logs diff --git a/forensic_viewer-php.bk b/forensic_viewer-php.bk new file mode 100644 index 0000000..8393cd2 --- /dev/null +++ b/forensic_viewer-php.bk @@ -0,0 +1,1493 @@ + 'stim@rssa.id', + 'name' => 'STIM RSSA Admin (Pengembangan SSO)', + 'email' => 'stim@rssa.id', + 'login_time' => date('Y-m-d H:i:s'), + 'auth_type' => 'temporary_local', + 'kc_server' => 'local', + 'kc_realm' => 'simrs', + ]; + + if (isset($forensic)) { + $forensic->logAuth('login_success_temporary', [ + 'user_nip' => 'stim@rssa.id', + 'auth_type' => 'temporary_local', + 'ip' => $_SERVER['REMOTE_ADDR'] ?? '', + ], 'INFO'); + } + + header('Location: forensic_viewer.php'); + exit; + } + + // 2. Autentikasi Direct Keycloak SSO + $baseServer = rtrim($kcServer, '/'); + if (strpos($baseServer, '/protocol/openid-connect') !== false) { + $tokenEndpoint = $baseServer; + } elseif (strpos($baseServer, '/realms/') !== false) { + $tokenEndpoint = $baseServer . '/protocol/openid-connect/token'; + } else { + $tokenEndpoint = $baseServer . '/realms/' . rawurlencode($kcRealm) . '/protocol/openid-connect/token'; + } + + $postParams = [ + 'grant_type' => 'password', + 'client_id' => $kcClientId, + 'username' => $username, + 'password' => $password, + ]; + if (!empty($kcClientSecret)) { + $postParams['client_secret'] = $kcClientSecret; + } + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $tokenEndpoint); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postParams)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 8); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlErr = curl_error($ch); + curl_close($ch); + + if ($curlErr) { + $loginError = 'Gagal terhubung ke Server Keycloak SSO (' . htmlspecialchars($tokenEndpoint) . '): ' . $curlErr; + } else { + $jsonData = json_decode($response, true); + if ($httpCode === 200 && !empty($jsonData['access_token'])) { + $tokenParts = explode('.', $jsonData['access_token']); + $payload = []; + if (isset($tokenParts[1])) { + $decoded = base64_decode(strtr($tokenParts[1], '-_', '+/')); + $payload = json_decode($decoded, true) ?: []; + } + + $_SESSION['forensic_user'] = [ + 'username' => $payload['preferred_username'] ?? $username, + 'name' => $payload['name'] ?? ($payload['preferred_username'] ?? $username), + 'email' => $payload['email'] ?? '', + 'login_time' => date('Y-m-d H:i:s'), + 'access_token' => $jsonData['access_token'], + 'auth_type' => 'keycloak_sso', + 'kc_server' => $kcServer, + 'kc_realm' => $kcRealm, + ]; + + if (isset($forensic)) { + $forensic->logAuth('login_success', [ + 'user_nip' => $username, + 'sso_type' => 'keycloak_direct', + 'realm' => $kcRealm, + 'ip' => $_SERVER['REMOTE_ADDR'] ?? '', + ], 'INFO'); + } + + header('Location: forensic_viewer.php'); + exit; + } else { + $errMsg = $jsonData['error_description'] ?? ($jsonData['error'] ?? 'Autentikasi Gagal (HTTP ' . $httpCode . ')'); + $loginError = 'Login Error: Username / Password tidak sesuai atau ' . $errMsg; + + if (isset($forensic)) { + $forensic->logAuth('login_failed', [ + 'user_nip' => $username, + 'sso_type' => 'keycloak_direct', + 'http_code' => $httpCode, + 'error' => $errMsg, + ], 'WARNING'); + } + } + } + } +} + +// Access Check: forensic_viewer.php WAJIB Login terlebih dahulu! (Direct access dilarang tanpa sesi aktif) +$isSsoLoggedIn = !empty($_SESSION['forensic_user']) || !empty($_SESSION['SES_REG']); + +// Form login Keycloak SSO & Local (Tampil otomatis jika pengguna belum terautentikasi) +if (!$isSsoLoggedIn): +?> + + + + + + + + + + Login Keycloak SSO — SIMRS Forensic Inspector + + + + + + +
+
+ +
+

SIMRS Forensic Inspector

+

Masuk menggunakan Keycloak SSO atau Akun Login Sementara

+ + + + + + + +
+ + +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+ +
+
+
+ + + +
+ +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+ + + +
+ + +
+ + Secret Shortcut: Ctrl + Alt + F / Ketik forensic + +
+ + +
+ + + + + + +run(); + $actionMessage = 'Job retention & backup berhasil dijalankan. (Forensic: ' . ($res['forensic'] ?? 0) . ' files, Activity: ' . ($res['activity'] ?? 0) . ' files archived)'; +} + +// Action: Clear Brute Force Lockout +if (isset($_POST['action']) && $_POST['action'] === 'clear_bf' && !empty($_POST['target_ip'])) { + if (isset($forensic)) { + $forensic->clearBruteForce($_POST['target_ip']); + $actionMessage = 'Lockout IP ' . htmlspecialchars($_POST['target_ip']) . ' berhasil dibersihkan.'; + } +} + +// Filters +$filterDate = $_GET['date'] ?? ''; +$filterChannel = $_GET['channel'] ?? 'all'; +$filterLevel = $_GET['level'] ?? 'all'; +$filterCorrelation = $_GET['correlation'] ?? ''; +$filterSearch = $_GET['search'] ?? ''; +$filterTimeStart = $_GET['time_start'] ?? ''; +$filterTimeEnd = $_GET['time_end'] ?? ''; +$filterSource = $_GET['source'] ?? 'auto'; + +// Sinkronisasi Tanggal Otomatis jika Memilih File Berkas Arsip ZIP Spesifik +$isSpecificZip = (strpos($filterSource, '.zip') !== false); +if ($isSpecificZip) { + if (preg_match('/(20\d{2}-\d{2})/', $filterSource, $zipYmMatch)) { + $zipYm = $zipYmMatch[1]; + if (!isset($_GET['date']) || strpos($_GET['date'], $zipYm) === false) { + $filterDate = $zipYm . '-01'; + } + } +} +if (empty($filterDate)) { + $filterDate = date('Y-m-d'); +} + +// Scan Backup Archives terlebih dahulu +$backupDir = _DOCROOT_ . 'logs/backup/'; +$backups = []; +if (is_dir($backupDir)) { + foreach (glob($backupDir . '*.zip') as $zipFile) { + $backups[] = [ + 'filename' => basename($zipFile), + 'size_mb' => round(filesize($zipFile) / 1024 / 1024, 2), + 'mtime' => date('Y-m-d H:i:s', filemtime($zipFile)), + ]; + } + // Urutkan arsip ZIP terbaru di atas (DESC by filename & date) + usort($backups, function ($a, $b) { + return strcmp($b['filename'], $a['filename']); + }); +} + +// Helper: Scan log files for selected date across all log directories +$logsRoot = _DOCROOT_ . 'logs/'; +$year = date('Y', strtotime($filterDate)); +$month = date('m', strtotime($filterDate)); +$day = date('d', strtotime($filterDate)); +$dmY = date('d-m-Y', strtotime($filterDate)); + +$logs = []; +$stats = [ + 'total' => 0, + 'login_success' => 0, + 'login_failed' => 0, + 'session_expired' => 0, + 'errors' => 0, + 'warnings' => 0, + 'performance_count' => 0, + 'avg_response_ms' => 0, +]; + +$totalRespMs = 0; +$extractLogMetaData = function(&$entry, $rawLine) { + if (!isset($entry['context']) || !is_array($entry['context'])) { + $entry['context'] = []; + } + + $msg = $entry['message'] ?? $rawLine; + $searchTarget = $rawLine . ' ' . $msg . ' ' . json_encode($entry['context']); + + // 1. EXTRACTION: IP ADDRESS + $extractedIp = $entry['context']['ip_address'] ?? ($entry['context']['ip'] ?? ($entry['context']['remote_addr'] ?? ($entry['ip_address'] ?? ($entry['ip'] ?? '')))); + + if (empty($extractedIp) || $extractedIp === '-') { + if (preg_match_all('/\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/', $searchTarget, $ipMatches)) { + $foundIps = array_unique($ipMatches[0]); + $bestIp = ''; + foreach ($foundIps as $ip) { + if ($ip !== '127.0.0.1' && $ip !== '0.0.0.0') { + $bestIp = $ip; + break; + } + } + $extractedIp = !empty($bestIp) ? $bestIp : reset($foundIps); + } + } + if (!empty($extractedIp)) { + $entry['context']['ip_address'] = $extractedIp; + } + + // 2. EXTRACTION: USER NIP / USERNAME / PETUGAS + $extractedNip = $entry['context']['user_nip'] ?? ($entry['context']['user'] ?? ($entry['context']['nip'] ?? ($entry['context']['username'] ?? ($entry['user_nip'] ?? ($entry['nip'] ?? ''))))); + + if (empty($extractedNip) || $extractedNip === '-') { + if (preg_match('/\["([^"]+)"\s*,\s*"([^"]*http[^"]*)"/i', $searchTarget, $mNameUrl)) { + $extractedNip = trim($mNameUrl[1]); + if (empty($entry['context']['request_uri'])) { + $entry['context']['request_uri'] = trim($mNameUrl[2]); + } + } elseif (preg_match('/(?:NIP|User|Petugas|Akun|oleh)[:\s]+([A-Za-z0-9_\-\.\,\@\s]{3,40})/i', $searchTarget, $mNip)) { + $extractedNip = trim($mNip[1]); + } + } + if (!empty($extractedNip)) { + $entry['context']['user_nip'] = $extractedNip; + } + + // 3. EXTRACTION: REQUEST URI / URL + $extractedUri = $entry['context']['request_uri'] ?? ($entry['context']['uri'] ?? ($entry['context']['url'] ?? ($entry['request_uri'] ?? ($entry['uri'] ?? '')))); + + if (empty($extractedUri) || $extractedUri === '-') { + if (preg_match('/https?:\/\/[^\s"\'\]]+/', $searchTarget, $mUrl)) { + $extractedUri = $mUrl[0]; + } elseif (preg_match('/\s(\/[a-zA-Z0-9_\-\/\.]+\.php[^\s"\'\]]*)/', $searchTarget, $mUri)) { + $extractedUri = $mUri[1]; + } + } + if (!empty($extractedUri)) { + $entry['context']['request_uri'] = $extractedUri; + } +}; + +$processLogLine = function ($line, $sourceChannel = 'access') use (&$logs, &$stats, &$totalRespMs, &$respCount, $filterLevel, $filterTimeStart, $filterTimeEnd, $filterCorrelation, $filterSearch, $filterChannel, $extractLogMetaData, $filterDate) { + $line = trim($line); + if (empty($line)) return; + + static $lineSeq = 0; + $lineSeq++; + + $entry = json_decode($line, true); + if (!$entry || !is_array($entry)) { + // Parsing log non-JSON (Access, Activity, Retention log) + $defaultTs = !empty($filterDate) ? $filterDate . ' 00:00:00' : date('Y-m-d H:i:s'); + $timestamp = ''; + + if (preg_match('/\[(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?)\]/', $line, $m)) { + $timestamp = str_replace('T', ' ', $m[1]); + } elseif (preg_match('/(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?)/', $line, $m)) { + $timestamp = str_replace('T', ' ', $m[1]); + } elseif (preg_match('/(\d{2}-\d{2}-\d{4}[ T]\d{2}:\d{2}:\d{2})/', $line, $m)) { + $timestamp = date('Y-m-d H:i:s', strtotime($m[1])); + } elseif (preg_match('/(\d{4}\/\d{2}\/\d{2}[ T]\d{2}:\d{2}:\d{2})/', $line, $m)) { + $timestamp = date('Y-m-d H:i:s', strtotime(str_replace('/', '-', $m[1]))); + } + + if (empty($timestamp)) { + $timestamp = $defaultTs; + } else { + if (preg_match('/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/', $timestamp, $tmClean)) { + $timestamp = $tmClean[1]; + } + } + + $entry = [ + 'timestamp' => $timestamp, + 'channel' => $sourceChannel ?: 'access', + 'level' => 'INFO', + 'message' => $line, + 'correlation_id' => '-', + 'context' => [ + 'raw_log' => $line + ] + ]; + } else { + if (empty($entry['timestamp'])) { + $tsVal = $entry['created_at'] ?? ($entry['date'] ?? ($entry['time'] ?? '')); + if (!empty($tsVal)) { + $entry['timestamp'] = date('Y-m-d H:i:s', strtotime($tsVal)); + } else { + if (preg_match('/(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2})/', $line, $m)) { + $entry['timestamp'] = str_replace('T', ' ', $m[1]); + } else { + $entry['timestamp'] = !empty($filterDate) ? $filterDate . ' 00:00:00' : date('Y-m-d H:i:s'); + } + } + } + } + + $entry['_seq'] = $lineSeq; + + // Ekstraksi metadata pintar (IP Address, User NIP, URI) + $extractLogMetaData($entry, $line); + + $ch = $entry['channel'] ?? $sourceChannel; + + if ($filterChannel !== 'all' && strtolower($ch) !== strtolower($filterChannel)) { + return; + } + + $lvl = strtoupper($entry['level'] ?? 'INFO'); + if ($filterLevel !== 'all' && $lvl !== strtoupper($filterLevel)) { + return; + } + + if ($filterTimeStart !== '' || $filterTimeEnd !== '') { + $entryTime = ''; + if (isset($entry['timestamp'])) { + if (preg_match('/[T ](\d{2}:\d{2})/', $entry['timestamp'], $timeMatch)) { + $entryTime = $timeMatch[1]; + } else { + $entryTime = date('H:i', strtotime($entry['timestamp'])); + } + } + if ($filterTimeStart !== '' && $entryTime !== '' && $entryTime < $filterTimeStart) { + return; + } + if ($filterTimeEnd !== '' && $entryTime !== '' && $entryTime > $filterTimeEnd) { + return; + } + } + + if ($filterCorrelation !== '' && ($entry['correlation_id'] ?? '') !== $filterCorrelation) { + return; + } + + if ($filterSearch !== '') { + $searchTarget = ($entry['message'] ?? '') . ' ' . + ($entry['channel'] ?? '') . ' ' . + ($entry['level'] ?? '') . ' ' . + ($entry['correlation_id'] ?? '') . ' ' . + ($entry['context']['ip_address'] ?? '') . ' ' . + ($entry['context']['user_nip'] ?? '') . ' ' . + ($entry['context']['request_uri'] ?? '') . ' ' . + json_encode($entry['context'] ?? [], JSON_UNESCAPED_SLASHES); + if (stripos($searchTarget, $filterSearch) === false) { + return; + } + } + + $stats['total']++; + if ($lvl === 'ERROR' || $lvl === 'CRITICAL') $stats['errors']++; + if ($lvl === 'WARNING') $stats['warnings']++; + + $msg = $entry['message'] ?? ''; + if (stripos($msg, 'login_success') !== false || stripos($msg, 'login ok') !== false || stripos($msg, 'login berhasil') !== false) $stats['login_success']++; + if (stripos($msg, 'login_failed') !== false || stripos($msg, 'gagal login') !== false) $stats['login_failed']++; + if (stripos($msg, 'session_expired') !== false || stripos($msg, 'session habis') !== false) $stats['session_expired']++; + + if (strtolower($ch) === 'performance') { + $stats['performance_count']++; + if (isset($entry['context']['total_duration_ms'])) { + $totalRespMs += $entry['context']['total_duration_ms']; + $respCount++; + } + } + + $logs[] = $entry; +}; + +// 1. Baca Forensic Logs (/app/logs/forensic/YYYY/MM/*.log) +if ($filterSource === 'auto' || $filterSource === 'active') { + $forensicDir = $logsRoot . 'forensic/' . $year . '/' . $month . '/'; + if (is_dir($forensicDir)) { + foreach (glob($forensicDir . '*.log') as $fPath) { + $fName = basename($fPath); + if (strpos($fName, $filterDate) !== false) { + $chName = explode('_', $fName)[0] ?? 'forensic'; + $handle = @fopen($fPath, 'r'); + if ($handle) { + while (($line = fgets($handle)) !== false) { + $processLogLine($line, $chName); + } + fclose($handle); + } + } + } + } +} + +// 2. Baca Activity Logs (/app/logs/activity/YYYY/MM/DD-MM-YYYY.log) +if ($filterSource === 'auto' || $filterSource === 'active') { + $activityPath = $logsRoot . 'activity/' . $year . '/' . $month . '/' . $dmY . '.log'; + if (file_exists($activityPath)) { + $handle = @fopen($activityPath, 'r'); + if ($handle) { + while (($line = fgets($handle)) !== false) { + $processLogLine($line, 'activity'); + } + fclose($handle); + } + } +} + +// 3. Baca Access Logs (/app/logs/access/YYYY/MM/DD/*.log) +if ($filterSource === 'auto' || $filterSource === 'active') { + $accessDir = $logsRoot . 'access/' . $year . '/' . $month . '/' . $day . '/'; + if (is_dir($accessDir)) { + foreach (glob($accessDir . '*.log') as $aPath) { + $aName = basename($aPath, '.log'); + $handle = @fopen($aPath, 'r'); + if ($handle) { + while (($line = fgets($handle)) !== false) { + $processLogLine($line, 'access'); + } + fclose($handle); + } + } + } +} + +// 4. Baca System Retention Log (/app/logs/retention.log) +if ($filterSource === 'auto' || $filterSource === 'active') { + $retentionPath = $logsRoot . 'retention.log'; + if (file_exists($retentionPath)) { + $handle = @fopen($retentionPath, 'r'); + if ($handle) { + while (($line = fgets($handle)) !== false) { + if (empty($filterDate) || strpos($line, $filterDate) !== false) { + $processLogLine($line, 'system'); + } + } + fclose($handle); + } + } +} + +// 5. Baca ZIP Archives (/app/logs/backup/*.zip) +$zipFilesToProcess = []; +if (is_dir($backupDir)) { + if (strpos($filterSource, '.zip') !== false && file_exists($backupDir . $filterSource)) { + $zipFilesToProcess[] = $backupDir . $filterSource; + } elseif ($filterSource === 'all_zip' || ($filterSource === 'auto' && empty($logs))) { + foreach (glob($backupDir . '*.zip') as $zFile) { + if ($filterSource === 'auto' && !empty($filterDate)) { + $ymMonth = date('Y-m', strtotime($filterDate)); + if (strpos(basename($zFile), $ymMonth) !== false) { + $zipFilesToProcess[] = $zFile; + } + } else { + $zipFilesToProcess[] = $zFile; + } + } + } +} + +if (!empty($zipFilesToProcess) && class_exists('ZipArchive')) { + $zip = new ZipArchive(); + foreach ($zipFilesToProcess as $zPath) { + if ($zip->open($zPath) === true) { + for ($i = 0; $i < $zip->numFiles; $i++) { + $entryName = $zip->getNameIndex($i); + if (!empty($filterDate) && $filterSource === 'auto') { + if (strpos($entryName, $filterDate) === false && + strpos($entryName, $dmY) === false && + strpos($entryName, '/' . $day . '/') === false) { + continue; + } + } + + $stream = $zip->getStream($entryName); + if ($stream) { + $ch = 'archive'; + if (strpos($entryName, 'session') !== false) $ch = 'session'; + elseif (strpos($entryName, 'auth') !== false) $ch = 'auth'; + elseif (strpos($entryName, 'error') !== false) $ch = 'error'; + elseif (strpos($entryName, 'performance') !== false) $ch = 'performance'; + elseif (strpos($entryName, 'security') !== false) $ch = 'security'; + elseif (strpos($entryName, 'access') !== false) $ch = 'access'; + elseif (strpos($entryName, 'activity') !== false) $ch = 'activity'; + + while (($line = fgets($stream)) !== false) { + $processLogLine($line, $ch); + } + fclose($stream); + } + } + $zip->close(); + } + } +} + +if ($respCount > 0) { + $stats['avg_response_ms'] = round($totalRespMs / $respCount, 2); +} + +// Sort logs strictly descending by timestamp (newest first), with sequence index as secondary tie-breaker +usort($logs, function ($a, $b) { + $tsA = strtotime($a['timestamp'] ?? '1970-01-01'); + $tsB = strtotime($b['timestamp'] ?? '1970-01-01'); + if ($tsA !== $tsB) { + return $tsB <=> $tsA; // Descending: newest log entry at the top + } + return ($b['_seq'] ?? 0) <=> ($a['_seq'] ?? 0); // Newest line in file on top +}); + +// Pagination Configuration & Lazy Load / Infinite Scroll (Limit 100 per page default) +$perPage = isset($_GET['limit']) ? (int)$_GET['limit'] : 100; +if (!in_array($perPage, [50, 100, 250, 500, 1000])) { + $perPage = 100; +} + +$totalLogs = count($logs); +$totalPages = (int)ceil($totalLogs / $perPage); +if ($totalPages < 1) $totalPages = 1; + +$currentPage = isset($_GET['page']) ? (int)$_GET['page'] : 1; +if ($currentPage < 1) $currentPage = 1; +if ($currentPage > $totalPages) $currentPage = $totalPages; + +$offset = ($currentPage - 1) * $perPage; +$pagedLogs = array_slice($logs, $offset, $perPage); + +// Helper function to render a single log row cleanly +$renderLogRow = function ($l, $idx) { + $lvl = $l['level'] ?? 'INFO'; + $ts = $l['timestamp'] ?? ''; + $timeStr = !empty($ts) ? date('H:i:s', strtotime($ts)) : '-'; + $ctx = $l['context'] ?? []; + $rowId = 'detail_' . $idx; + + $msg = $l['message'] ?? ''; + $isLong = mb_strlen($msg) > 130; + $msgShort = $isLong ? mb_substr($msg, 0, 130) : $msg; + $msgId = 'msg_' . $idx; + ?> + +
+ + + + + +
+ +
+ ... + + Detail + +
+ + + + +
+ +
+ +
+ + + + + + + + +
+ Data Log JSON Structure (Row #) + +
+
+
+
+ + + $l) { + $renderLogRow($l, $offset + $i); + } + $html = ob_get_clean(); + header('Content-Type: application/json'); + echo json_encode([ + 'html' => $html, + 'currentPage' => $currentPage, + 'totalPages' => $totalPages, + 'hasNext' => $currentPage < $totalPages, + 'nextPage' => $currentPage + 1, + 'totalLogs' => $totalLogs, + 'countLoaded' => count($pagedLogs), + ]); + exit; +} + +// Scan Brute Force cache +$bfDir = $logsRoot . 'forensic/cache/'; +$lockedIPs = []; +if (is_dir($bfDir)) { + foreach (glob($bfDir . 'bf_*.json') as $bfFile) { + $bfData = json_decode(@file_get_contents($bfFile), true); + if ($bfData && isset($bfData['lockout_until']) && $bfData['lockout_until'] > time()) { + $lockedIPs[] = $bfData; + } + } +} + +// Export CSV / JSON action +if (isset($_GET['export'])) { + if ($_GET['export'] === 'json') { + header('Content-Type: application/json'); + header('Content-Disposition: attachment; filename="forensic_log_' . $filterDate . '.json"'); + echo json_encode($logs, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + exit; + } elseif ($_GET['export'] === 'csv') { + header('Content-Type: text/csv'); + header('Content-Disposition: attachment; filename="forensic_log_' . $filterDate . '.csv"'); + $out = fopen('php://output', 'w'); + fputcsv($out, ['Timestamp', 'Correlation ID', 'Channel', 'Level', 'Message', 'IP', 'NIP', 'URI']); + foreach ($logs as $l) { + fputcsv($out, [ + $l['timestamp'] ?? '', + $l['correlation_id'] ?? '', + $l['channel'] ?? '', + $l['level'] ?? '', + $l['message'] ?? '', + $l['context']['ip_address'] ?? '', + $l['context']['user_nip'] ?? '', + $l['context']['request_uri'] ?? '', + ]); + } + fclose($out); + exit; + } +} + +$currentUser = $_SESSION['forensic_user']['username'] ?? ($_SESSION['SES_REG'] ?? 'Admin'); +?> + + + + + + + + + + SIMRS Forensic Inspector + + + + + + + + +
+ + + + + + +
+
+
+
Total Event
+
+
+
+
+
+
+
Login Sukses
+
+
Gagal:
+
+
+
+
+
Session Expired
+
+
Navigasi Terhalang
+
+
+
+
+
Error & Critical
+
+
Warning:
+
+
+
+
+
Rata² Response
+
ms
+
Request
+
+
+
+
+
Brute Force Locked
+
+
IP Terblokir
+
+
+
+ + + +
+
+ Peringatan Keamanan: Perangkat / IP Terblokir (Brute Force Detected) +
+
+ + + + + + + + + + + + + + + + + + + +
IP AddressTotal GagalSisa Waktu LockoutAksi Unblock
x Percobaan Gagal + + +
+ + + +
+
+
+
+ + + +
+
+
+
+ + +
+
+ + +
+
+ +
+ +
+ - +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+ +
+
+ +
+
+
+ Tanggal Log: + | + + Menampilkan 0 ? $offset + 1 : 0 ?> - dari baris (Halaman dari ) + +
+
+ +
+ $v): ?> + + + + + + +
+ + Export CSV + Export JSON +
+
+
+ + + + + + + + + + + + + + + + + + + $l): ?> + + + + +
WaktuLevelChannelIP AddressUser NIPPesan / EventDetail
+ + Tidak ada log ditemukan untuk kriteria filter ini. +
+
+ + + 0): ?> +
+
+ Menampilkan Halaman dari (Total log) +
+ + 1): ?> + + + + + + + +
+ +
+ + +
+
+
+ Berkas log berusia >30 hari diarsipkan secara kompresi per bulan. +
+
+ + +
+
+ +
+ + Belum ada file arsip log zip di folder logs/backup/. File log berusia >30 hari akan diarsipkan otomatis. +
+ +
+ + + + + + + + + + + + + + + + + + + +
Nama Berkas ArsipUkuran FileWaktu PembuatanAksi & Status
MB + + Baca Log ZIP + + Safe Archive +
+
+ +
+
+
+
+ +
+ + + + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..9513776 --- /dev/null +++ b/index.html @@ -0,0 +1,16 @@ + + + + + + + SIMRS Logs Inspector — Standalone Dashboard + + + + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..aa2da5a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3667 @@ +{ + "name": "log-forensic", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "log-forensic", + "version": "1.0.0", + "dependencies": { + "adm-zip": "^0.5.16", + "axios": "^1.7.9", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "dotenv": "^16.4.7", + "express": "^4.21.2", + "lucide-react": "^0.475.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "sweetalert2": "^11.26.25" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.5", + "@types/adm-zip": "^0.5.5", + "@types/cookie-parser": "^1.4.8", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/node": "^22.13.1", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react": "^4.3.4", + "tailwindcss": "^4.0.5", + "typescript": "^5.7.3", + "vite": "^6.1.0" + }, + "engines": { + "node": ">=23.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "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.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@types/adm-zip": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.8.tgz", + "integrity": "sha512-RVVH7QvZYbN+ihqZ4kX/dMiowf6o+Jk1fNwiSdx0NahBJLU787zkULhGhJM8mf/obmLGmgdMM0bXsQTmyfbR7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", + "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz", + "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/adm-zip": { + "version": "0.5.18", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.18.tgz", + "integrity": "sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.11.tgz", + "integrity": "sha512-/yImnXwyTvgMkhgekLHok/Rx5vO6E0BmStWlSqKWMVm2a2ITuZ1Tn+9bgLS+gZRdZmWtd8nxuhHpdmCUOWsTQQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.399", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.399.tgz", + "integrity": "sha512-lEcqhErbHjXRvd41rnWLpzbyU/IXfIYo7QwaFWmxGeLiLyY2TBCdHnWY88vB+p3ubnihRypDm66panXl7TylLA==", + "dev": true, + "license": "ISC" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "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-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "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/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.475.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.475.0.tgz", + "integrity": "sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "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/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", + "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.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/sweetalert2": { + "version": "11.26.25", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.26.25.tgz", + "integrity": "sha512-+hunCOJdJ6FLj04T9YSLvvZXRjsvIkTeTKP2e4VF8CaBias961BTnWiSFAy7F/CM5eq3QK2Rraoc5Gzftslvkg==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/limonte" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1c0216e --- /dev/null +++ b/package.json @@ -0,0 +1,42 @@ +{ + "name": "log-forensic", + "private": true, + "version": "1.0.0", + "type": "module", + "engines": { + "node": ">=23.0.0" + }, + "scripts": { + "dev": "vite", + "server": "node server/index.js", + "build": "tsc && vite build", + "preview": "vite preview", + "start": "node server/index.js" + }, + "dependencies": { + "adm-zip": "^0.5.16", + "axios": "^1.7.9", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "dotenv": "^16.4.7", + "express": "^4.21.2", + "lucide-react": "^0.475.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "sweetalert2": "^11.26.25" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.5", + "@types/adm-zip": "^0.5.5", + "@types/cookie-parser": "^1.4.8", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/node": "^22.13.1", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react": "^4.3.4", + "tailwindcss": "^4.0.5", + "typescript": "^5.7.3", + "vite": "^6.1.0" + } +} diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..5ed9f10 --- /dev/null +++ b/server/index.js @@ -0,0 +1,687 @@ +import express from "express"; +import cors from "cors"; +import cookieParser from "cookie-parser"; +import dotenv from "dotenv"; +import path from "path"; +import fs from "fs"; +import crypto from "crypto"; +import http from "http"; +import axios from "axios"; +import { fileURLToPath } from "url"; +import { + scanLogs, + getBruteForceLocks, + unblockIp, + getBackupArchives, +} from "./logParser.js"; + +dotenv.config(); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const app = express(); +const server = http.createServer(app); + +const PORT = process.env.PORT || 5880; +const LOGS_DIR = path.resolve(process.env.LOGS_DIR || "./logs"); +const isDev = + process.env.NODE_ENV !== "production" || process.env.VITE_DEV === "true"; + +app.use(cors({ origin: true, credentials: true })); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use(cookieParser()); + +// Default Keycloak Env Config +const defaultKcServer = process.env.KEYCLOAK_URL || "https://auth.rssa.top/"; +const defaultKcRealm = process.env.KEYCLOAK_REALM || "rssa"; +const defaultKcClient = process.env.KEYCLOAK_CLIENT_ID || "satu"; +const defaultKcSecret = + process.env.KEYCLOAK_CLIENT_SECRET || "ZhkK45MHB0a0eAZX5ecNTnlfnWlZXfBE"; + +const SESSION_COOKIE = "forensic_session"; +const PKCE_STATE_COOKIE = "pkce_auth_state"; + +// Helper for Session cookie +function getSessionUser(req) { + const sessionStr = req.cookies?.[SESSION_COOKIE]; + if (!sessionStr) return null; + try { + return JSON.parse(Buffer.from(sessionStr, "base64").toString("utf-8")); + } catch (e) { + return null; + } +} + +function setSessionUser(res, user) { + const encoded = Buffer.from(JSON.stringify(user)).toString("base64"); + res.cookie(SESSION_COOKIE, encoded, { + httpOnly: true, + secure: false, + sameSite: "lax", + maxAge: 86400 * 1000, + }); +} + +// Cryptographic Helpers for PKCE +function base64UrlEncode(buffer) { + return buffer + .toString("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); +} + +function generateCodeVerifier() { + return base64UrlEncode(crypto.randomBytes(32)); +} + +function generateCodeChallenge(verifier) { + const hash = crypto.createHash("sha256").update(verifier).digest(); + return base64UrlEncode(hash); +} + +// Keycloak Group Verification Helper (Restricted to "Instalasi STIM" / "STIM") +async function extractAndVerifyUserGroups(payload, accessToken, serverUrl, realm) { + const groups = new Set(); + + const checkClaim = (val) => { + if (Array.isArray(val)) { + val.forEach((item) => { + if (typeof item === "string") groups.add(item); + else if (item && item.name) groups.add(item.name); + }); + } else if (typeof val === "string") { + groups.add(val); + } + }; + + checkClaim(payload.groups); + checkClaim(payload.group); + checkClaim(payload.realm_access?.roles); + if (payload.resource_access) { + Object.values(payload.resource_access).forEach((resAcc) => { + checkClaim(resAcc?.roles); + }); + } + + // If no groups found in token payload, query Keycloak UserInfo Endpoint + if (accessToken && serverUrl && realm) { + try { + const cleanServer = serverUrl.replace(/\/$/, ""); + const userinfoUrl = `${cleanServer}/realms/${encodeURIComponent(realm)}/protocol/openid-connect/userinfo`; + const res = await axios.get(userinfoUrl, { + headers: { Authorization: `Bearer ${accessToken}` }, + timeout: 5000, + }); + if (res.data) { + checkClaim(res.data.groups); + checkClaim(res.data.group); + checkClaim(res.data.realm_access?.roles); + } + } catch (e) { + console.warn("Keycloak UserInfo fetch warning:", e.message); + } + } + + const groupList = Array.from(groups); + console.log("Extracted Keycloak user groups:", groupList); + + // Group STIM Authorization check + const isAllowed = groupList.some((g) => { + const lower = String(g).toLowerCase(); + return lower.includes("stim") || lower.includes("instalasi stim"); + }); + + return { isAllowed, groupList }; +} + +if (!fs.existsSync(LOGS_DIR)) { + fs.mkdirSync(LOGS_DIR, { recursive: true }); +} + +// ------------------------------------------------------------- +// REST API ENDPOINTS +// ------------------------------------------------------------- + +// 1. PKCE Keycloak SSO Initiate Route +app.get("/api/auth/pkce/login", (req, res) => { + const serverUrl = (req.query.kc_server || defaultKcServer) + .toString() + .replace(/\/$/, ""); + const realm = (req.query.kc_realm || defaultKcRealm).toString(); + const clientId = (req.query.kc_client_id || defaultKcClient).toString(); + const clientSecret = ( + req.query.kc_client_secret || defaultKcSecret + ).toString(); + + const codeVerifier = generateCodeVerifier(); + const codeChallenge = generateCodeChallenge(codeVerifier); + const state = base64UrlEncode(crypto.randomBytes(16)); + + const proto = req.headers["x-forwarded-proto"] || req.protocol || "http"; + const host = req.headers["x-forwarded-host"] || req.headers.host; + + let redirectUri = ( + req.query.kc_redirect_uri || + process.env.KEYCLOAK_REDIRECT_URI || + "" + ).toString(); + if (!redirectUri) { + redirectUri = `${proto}://${host}/api/auth/pkce/callback`; + } + + const pkceState = { + codeVerifier, + state, + redirectUri, + serverUrl, + realm, + clientId, + clientSecret, + }; + + res.cookie( + PKCE_STATE_COOKIE, + Buffer.from(JSON.stringify(pkceState)).toString("base64"), + { + httpOnly: true, + secure: false, + sameSite: "lax", + maxAge: 600 * 1000, + }, + ); + + const authEndpoint = `${serverUrl}/realms/${encodeURIComponent(realm)}/protocol/openid-connect/auth`; + const authUrl = + `${authEndpoint}?` + + new URLSearchParams({ + response_type: "code", + client_id: clientId, + redirect_uri: redirectUri, + scope: "openid profile email", + state: state, + code_challenge: codeChallenge, + code_challenge_method: "S256", + }).toString(); + + return res.redirect(authUrl); +}); + +// 2. PKCE Keycloak SSO Callback Route +app.get("/api/auth/pkce/callback", async (req, res) => { + const { code, state, error, error_description } = req.query; + + if (error) { + res.clearCookie(PKCE_STATE_COOKIE); + return res.redirect( + `/?error=${encodeURIComponent(error_description || error)}`, + ); + } + + const pkceCookie = req.cookies?.[PKCE_STATE_COOKIE]; + if (!pkceCookie || !code || !state) { + res.clearCookie(PKCE_STATE_COOKIE); + return res.redirect("/?error=Sesi+PKCE+tidak+valid+atau+kadaluarsa."); + } + + let pkceState = null; + try { + pkceState = JSON.parse(Buffer.from(pkceCookie, "base64").toString("utf-8")); + } catch (e) { + res.clearCookie(PKCE_STATE_COOKIE); + return res.redirect("/?error=Gagal+membaca+state+PKCE."); + } + + if (pkceState.state !== state) { + res.clearCookie(PKCE_STATE_COOKIE); + return res.redirect("/?error=Validasi+CSRF+State+PKCE+Gagal."); + } + + const tokenEndpoint = `${pkceState.serverUrl}/realms/${encodeURIComponent(pkceState.realm)}/protocol/openid-connect/token`; + + const params = new URLSearchParams(); + params.append("grant_type", "authorization_code"); + params.append("client_id", pkceState.clientId); + params.append("code", code.toString()); + params.append("redirect_uri", pkceState.redirectUri); + params.append("code_verifier", pkceState.codeVerifier); + if (pkceState.clientSecret) { + params.append("client_secret", pkceState.clientSecret); + } + + try { + const tokenRes = await axios.post(tokenEndpoint, params.toString(), { + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + timeout: 10000, + }); + + if (tokenRes.data && tokenRes.data.access_token) { + let payload = {}; + const parts = tokenRes.data.access_token.split("."); + if (parts[1]) { + try { + payload = JSON.parse( + Buffer.from(parts[1], "base64").toString("utf-8"), + ); + } catch (e) {} + } + + const username = payload.preferred_username || payload.sub || "User"; + + // Verify STIM Group Access + const { isAllowed, groupList } = await extractAndVerifyUserGroups( + payload, + tokenRes.data.access_token, + pkceState.serverUrl, + pkceState.realm, + ); + + if (!isAllowed) { + res.clearCookie(PKCE_STATE_COOKIE); + const groupInfo = groupList.length > 0 ? groupList.join(", ") : "Tanpa Grup"; + return res.redirect( + `/?error=${encodeURIComponent(`Akses Ditolak: User "${username}" (${groupInfo}) tidak memiliki akses grup Instalasi STIM.`)}`, + ); + } + + const user = { + username: username, + name: payload.name || username, + email: payload.email || "", + login_time: new Date().toISOString().replace("T", " ").substring(0, 19), + access_token: tokenRes.data.access_token, + auth_type: "keycloak_sso_pkce", + kc_server: pkceState.serverUrl, + kc_realm: pkceState.realm, + groups: groupList, + }; + + setSessionUser(res, user); + res.clearCookie(PKCE_STATE_COOKIE); + return res.redirect("/"); + } else { + res.clearCookie(PKCE_STATE_COOKIE); + return res.redirect("/?error=Penukaran+Token+PKCE+Gagal."); + } + } catch (err) { + const errMsg = + err.response?.data?.error_description || + err.response?.data?.error || + err.message; + res.clearCookie(PKCE_STATE_COOKIE); + return res.redirect( + `/?error=Penukaran+Token+PKCE+Error:+${encodeURIComponent(errMsg)}`, + ); + } +}); + +// 3. Direct Login Endpoint (Fallback / Temporary Local) +app.post("/api/auth/login", async (req, res) => { + const { + username, + password, + kc_server, + kc_realm, + kc_client_id, + kc_client_secret, + } = req.body; + + if (!username || !password) { + return res + .status(400) + .json({ error: "Username dan Password wajib diisi." }); + } + + // A. Temporary Local Login + if ( + (username === "stim@rssa.id" || username === "stim") && + password === "RSSAjaya2026" + ) { + const user = { + username: "stim@rssa.id", + name: "STIM RSSA Admin (Pengembangan SSO)", + email: "stim@rssa.id", + login_time: new Date().toISOString().replace("T", " ").substring(0, 19), + auth_type: "temporary_local", + kc_server: "local", + kc_realm: "simrs", + groups: ["Instalasi STIM"], + }; + setSessionUser(res, user); + return res.json({ success: true, user }); + } + + // B. Keycloak Direct SSO Login (Fall-back Direct Token) + const serverUrl = (kc_server || defaultKcServer).replace(/\/$/, ""); + const realm = kc_realm || defaultKcRealm; + const clientId = kc_client_id || defaultKcClient; + const clientSecret = kc_client_secret || defaultKcSecret; + + let tokenEndpoint = ""; + if (serverUrl.includes("/protocol/openid-connect")) { + tokenEndpoint = serverUrl; + } else if (serverUrl.includes("/realms/")) { + tokenEndpoint = `${serverUrl}/protocol/openid-connect/token`; + } else { + tokenEndpoint = `${serverUrl}/realms/${encodeURIComponent(realm)}/protocol/openid-connect/token`; + } + + const params = new URLSearchParams(); + params.append("grant_type", "password"); + params.append("client_id", clientId); + params.append("username", username); + params.append("password", password); + params.append("scope", "openid profile email"); + if (clientSecret) { + params.append("client_secret", clientSecret); + } + + try { + const response = await axios.post(tokenEndpoint, params.toString(), { + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + timeout: 10000, + }); + + if (response.data && response.data.access_token) { + let payload = {}; + const parts = response.data.access_token.split("."); + if (parts[1]) { + try { + payload = JSON.parse( + Buffer.from(parts[1], "base64").toString("utf-8"), + ); + } catch (e) {} + } + + const userUsername = payload.preferred_username || username; + + // Verify STIM Group Access + const { isAllowed, groupList } = await extractAndVerifyUserGroups( + payload, + response.data.access_token, + serverUrl, + realm, + ); + + if (!isAllowed) { + const groupInfo = groupList.length > 0 ? groupList.join(", ") : "Tanpa Grup"; + return res.status(403).json({ + error: `Akses Ditolak: User "${userUsername}" (${groupInfo}) tidak memiliki akses grup Instalasi STIM.`, + }); + } + + const user = { + username: userUsername, + name: payload.name || payload.preferred_username || username, + email: payload.email || "", + login_time: new Date().toISOString().replace("T", " ").substring(0, 19), + access_token: response.data.access_token, + auth_type: "keycloak_sso", + kc_server: serverUrl, + kc_realm: realm, + groups: groupList, + }; + + setSessionUser(res, user); + return res.json({ success: true, user }); + } else { + return res + .status(401) + .json({ error: "Keycloak SSO: Token tidak valid." }); + } + } catch (err) { + const errMsg = + err.response?.data?.error_description || + err.response?.data?.error || + err.message; + return res.status(401).json({ error: `Login Keycloak Gagal: ${errMsg}` }); + } +}); + +app.post("/api/auth/logout", (req, res) => { + res.clearCookie(SESSION_COOKIE); + res.clearCookie(PKCE_STATE_COOKIE); + res.json({ success: true }); +}); + +app.get("/api/auth/me", (req, res) => { + const envKcServer = process.env.KEYCLOAK_URL || process.env.SSO_URL; + const envKcRealm = process.env.KEYCLOAK_REALM; + const envKcClient = process.env.KEYCLOAK_CLIENT_ID; + const isKcEnvConfigured = Boolean(envKcServer || (envKcRealm && envKcClient)); + const simrsUrl = process.env.SIMRS_URL || "/"; + + const user = getSessionUser(req); + const config = { + isKcEnvConfigured, + defaultKcServer, + defaultKcRealm, + defaultKcClient, + defaultKcSecret, + simrsUrl, + }; + + if (!user) { + return res.json({ authenticated: false, config }); + } + return res.json({ authenticated: true, user, config }); +}); + +// 4. Scan Logs API +app.get("/api/logs", (req, res) => { + const { + source = "auto", + date = new Date().toISOString().substring(0, 10), + channel = "all", + level = "all", + time_start = "", + time_end = "", + search = "", + page = "1", + limit = "100", + } = req.query; + + const filterOpts = { + source: String(source), + date: String(date), + channel: String(channel), + level: String(level), + timeStart: String(time_start), + timeEnd: String(time_end), + search: String(search), + }; + + const { logs, stats } = scanLogs(LOGS_DIR, filterOpts); + const lockedIPs = getBruteForceLocks(LOGS_DIR); + const backups = getBackupArchives(LOGS_DIR); + + const totalLogs = stats.total; + const perPage = parseInt(String(limit), 10) || 100; + const totalPages = Math.max(1, Math.ceil(logs.length / perPage)); + const currentPage = Math.min( + Math.max(1, parseInt(String(page), 10) || 1), + totalPages, + ); + const offset = (currentPage - 1) * perPage; + + const pagedLogs = logs.slice(offset, offset + perPage); + + res.json({ + logs: pagedLogs, + stats: { + ...stats, + brute_force_locked: lockedIPs.length, + }, + pagination: { + totalLogs, + perPage, + currentPage, + totalPages, + hasNext: currentPage < totalPages, + offset, + }, + backups, + lockedIPs, + }); +}); + +// 5. Brute Force API +app.get("/api/brute-force", (req, res) => { + const lockedIPs = getBruteForceLocks(LOGS_DIR); + res.json({ lockedIPs }); +}); + +app.post("/api/brute-force/unblock", (req, res) => { + const { target_ip } = req.body; + if (!target_ip) { + return res.status(400).json({ error: "target_ip wajib diisi." }); + } + const success = unblockIp(LOGS_DIR, target_ip); + if (success) { + res.json({ + success: true, + message: `Lockout IP ${target_ip} berhasil dibersihkan.`, + }); + } else { + res + .status(404) + .json({ error: `IP ${target_ip} tidak ditemukan dalam cache lockout.` }); + } +}); + +// 6. Manual Retention Trigger API +app.post("/api/retention/run", (req, res) => { + const retentionPath = path.join(LOGS_DIR, "retention.log"); + const nowStr = new Date().toISOString().replace("T", " ").substring(0, 19); + const logEntry = `[${nowStr}] [INFO] [system] Manual retention job executed via API.\n`; + try { + fs.appendFileSync(retentionPath, logEntry); + res.json({ + success: true, + message: + "Job retention & backup berhasil dijalankan (Manual Retention Triggered).", + }); + } catch (err) { + res + .status(500) + .json({ error: `Gagal menjalankan retention job: ${err.message}` }); + } +}); + +// 7. Backups Listing API +app.get("/api/backups", (req, res) => { + const backups = getBackupArchives(LOGS_DIR); + res.json({ backups }); +}); + +// 8. Export API +app.get("/api/export", (req, res) => { + const { + type = "json", + source = "auto", + date = new Date().toISOString().substring(0, 10), + channel = "all", + level = "all", + search = "", + } = req.query; + + const filterOpts = { + source: String(source), + date: String(date), + channel: String(channel), + level: String(level), + search: String(search), + }; + + const { logs } = scanLogs(LOGS_DIR, filterOpts); + + if (type === "csv") { + res.setHeader("Content-Type", "text/csv"); + res.setHeader( + "Content-Disposition", + `attachment; filename="forensic_log_${date}.csv"`, + ); + + let csv = "Timestamp,Correlation ID,Channel,Level,Message,IP,NIP,URI\n"; + for (const l of logs) { + const ts = `"${(l.timestamp || "").replace(/"/g, '""')}"`; + const cid = `"${(l.correlation_id || "").replace(/"/g, '""')}"`; + const ch = `"${(l.channel || "").replace(/"/g, '""')}"`; + const lvl = `"${(l.level || "").replace(/"/g, '""')}"`; + const msg = `"${(l.message || "").replace(/"/g, '""')}"`; + const ip = `"${(l.context?.ip_address || "").replace(/"/g, '""')}"`; + const nip = `"${(l.context?.user_nip || "").replace(/"/g, '""')}"`; + const uri = `"${(l.context?.request_uri || "").replace(/"/g, '""')}"`; + csv += `${ts},${cid},${ch},${lvl},${msg},${ip},${nip},${uri}\n`; + } + return res.send(csv); + } else { + res.setHeader("Content-Type", "application/json"); + res.setHeader( + "Content-Disposition", + `attachment; filename="forensic_log_${date}.json"`, + ); + return res.json(logs); + } +}); + +// ------------------------------------------------------------- +// STATIC DIST SERVING (PRIMARY) VS VITE HMR DEV MIDDLEWARE +// ------------------------------------------------------------- +const distDir = path.resolve(__dirname, "../dist"); + +if (fs.existsSync(distDir) && process.env.VITE_DEV !== "true") { + app.use(express.static(distDir)); + app.get("*", (req, res, next) => { + if (req.path.startsWith("/api")) return next(); + res.sendFile(path.join(distDir, "index.html")); + }); +} else if (isDev) { + try { + const { createServer: createViteServer } = await import("vite"); + const vite = await createViteServer({ + server: { + middlewareMode: true, + allowedHosts: true, + hmr: { + server: server, + }, + watch: { + usePolling: true, + interval: 500, + ignored: ["**/logs/**", "**/node_modules/**", "**/dist/**", "**/.git/**"], + }, + }, + appType: "custom", + }); + + app.use(vite.middlewares); + + app.get("*", async (req, res, next) => { + if (req.path.startsWith("/api")) return next(); + try { + const url = req.originalUrl; + const indexPath = path.resolve(__dirname, "../index.html"); + let template = fs.readFileSync(indexPath, "utf-8"); + template = await vite.transformIndexHtml(url, template); + res.status(200).set({ "Content-Type": "text/html" }).end(template); + } catch (e) { + vite.ssrFixStacktrace(e); + next(e); + } + }); + } catch (err) { + console.error( + "Failed to start Vite Dev Middleware, falling back to static:", + err.message, + ); + } +} + +server.listen(PORT, "0.0.0.0", () => { + console.log( + `⚡ SIMRS Forensic Inspector Server running on http://0.0.0.0:${PORT} [ENV: ${process.env.NODE_ENV || "development"}]`, + ); +}); diff --git a/server/logParser.js b/server/logParser.js new file mode 100644 index 0000000..38dba75 --- /dev/null +++ b/server/logParser.js @@ -0,0 +1,380 @@ +import fs from 'fs'; +import path from 'path'; +import AdmZip from 'adm-zip'; + +// Pre-compiled regex instances for ultra-fast matching +const IP_REGEX = /\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/; +const NIP_REGEX = /(?:NIP|User|Petugas|Akun|oleh)[:\s]+([A-Za-z0-9_\-\.\,\@\s]{3,40})/i; +const URI_REGEX = /\s(\/[a-zA-Z0-9_\-\/\.]+\.php[^\s"']*)/; +const URL_REGEX = /https?:\/\/[^\s"']+/; + +export function extractLogMetaData(entry, rawLine) { + if (!entry.context || typeof entry.context !== 'object') { + entry.context = {}; + } + + // Fast path: if metadata is already populated, skip scanning + const hasIp = Boolean(entry.context.ip_address || entry.context.ip || entry.ip_address); + const hasNip = Boolean(entry.context.user_nip || entry.context.user || entry.context.nip || entry.context.username); + const hasUri = Boolean(entry.context.request_uri || entry.context.uri || entry.context.url); + + if (hasIp && hasNip && hasUri) return; + + const msg = entry.message || rawLine; + + if (!hasIp) { + const ipMatch = rawLine.match(IP_REGEX) || msg.match(IP_REGEX); + if (ipMatch && ipMatch[0] !== '127.0.0.1' && ipMatch[0] !== '0.0.0.0') { + entry.context.ip_address = ipMatch[0]; + } + } + + if (!hasNip) { + const mNip = rawLine.match(NIP_REGEX) || msg.match(NIP_REGEX); + if (mNip) { + entry.context.user_nip = mNip[1].trim(); + } + } + + if (!hasUri) { + const mUri = rawLine.match(URI_REGEX) || msg.match(URI_REGEX) || rawLine.match(URL_REGEX) || msg.match(URL_REGEX); + if (mUri) { + entry.context.request_uri = mUri[1] || mUri[0]; + } + } +} + +export function parseLogLine(line, sourceChannel, filterOpts, lineSeq) { + line = line.trim(); + if (!line) return null; + + let entry = null; + try { + entry = JSON.parse(line); + } catch (e) { + // Non-JSON line parsing + const defaultTs = filterOpts.date ? `${filterOpts.date} 00:00:00` : new Date().toISOString().replace('T', ' ').substring(0, 19); + let timestamp = ''; + + const m1 = line.match(/\[(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?)\]/); + const m2 = line.match(/(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?)/); + const m3 = line.match(/(\d{2}-\d{2}-\d{4}[ T]\d{2}:\d{2}:\d{2})/); + + if (m1) timestamp = m1[1].replace('T', ' '); + else if (m2) timestamp = m2[1].replace('T', ' '); + else if (m3) { + const parts = m3[1].split(/[ T]/); + const dParts = parts[0].split('-'); + timestamp = `${dParts[2]}-${dParts[1]}-${dParts[0]} ${parts[1]}`; + } + + if (!timestamp) timestamp = defaultTs; + else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/.test(timestamp)) { + timestamp = timestamp.substring(0, 19); + } + + entry = { + timestamp, + channel: sourceChannel || 'access', + level: 'INFO', + message: line, + correlation_id: '-', + context: { raw_log: line }, + }; + } + + if (entry && !entry.timestamp) { + const tsVal = entry.created_at || entry.date || entry.time; + if (tsVal) { + entry.timestamp = new Date(tsVal).toISOString().replace('T', ' ').substring(0, 19); + } else { + const m = line.match(/(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2})/); + entry.timestamp = m ? m[1].replace('T', ' ') : (filterOpts.date ? `${filterOpts.date} 00:00:00` : new Date().toISOString().replace('T', ' ').substring(0, 19)); + } + } + + entry._seq = lineSeq; + extractLogMetaData(entry, line); + + const ch = entry.channel || sourceChannel; + if (filterOpts.channel && filterOpts.channel !== 'all' && ch.toLowerCase() !== filterOpts.channel.toLowerCase()) { + return null; + } + + const lvl = (entry.level || 'INFO').toUpperCase(); + if (filterOpts.level && filterOpts.level !== 'all' && lvl !== filterOpts.level.toUpperCase()) { + return null; + } + + if (filterOpts.timeStart || filterOpts.timeEnd) { + let entryTime = ''; + const tmMatch = entry.timestamp.match(/[T ](\d{2}:\d{2})/); + if (tmMatch) entryTime = tmMatch[1]; + if (filterOpts.timeStart && entryTime && entryTime < filterOpts.timeStart) return null; + if (filterOpts.timeEnd && entryTime && entryTime > filterOpts.timeEnd) return null; + } + + if (filterOpts.search) { + const searchLow = filterOpts.search.toLowerCase(); + const msgLow = (entry.message || '').toLowerCase(); + const chLow = (entry.channel || '').toLowerCase(); + const lvlLow = (entry.level || '').toLowerCase(); + const ipLow = (entry.context?.ip_address || '').toLowerCase(); + const nipLow = (entry.context?.user_nip || '').toLowerCase(); + + if ( + !msgLow.includes(searchLow) && + !chLow.includes(searchLow) && + !lvlLow.includes(searchLow) && + !ipLow.includes(searchLow) && + !nipLow.includes(searchLow) + ) { + return null; + } + } + + return entry; +} + +export function scanLogs(logsRootDir, filterOpts) { + const filterDate = filterOpts.date || new Date().toISOString().substring(0, 10); + const [year, month, day] = filterDate.split('-'); + const dmY = `${day}-${month}-${year}`; + + const logs = []; + const MAX_BROWSER_ITEMS = 100000; // Limit for browser payload only, not scanning + + const stats = { + total: 0, + login_success: 0, + login_failed: 0, + session_expired: 0, + errors: 0, + warnings: 0, + performance_count: 0, + avg_response_ms: 0, + }; + + let totalRespMs = 0; + let respCount = 0; + let lineSeq = 0; + + const processLine = (line, ch) => { + + lineSeq++; + const item = parseLogLine(line, ch, filterOpts, lineSeq); + if (!item) return; + + stats.total++; + const lvl = (item.level || 'INFO').toUpperCase(); + if (lvl === 'ERROR' || lvl === 'CRITICAL') stats.errors++; + if (lvl === 'WARNING') stats.warnings++; + + const msg = item.message || ''; + if (/login_success|login ok|login berhasil/i.test(msg)) stats.login_success++; + if (/login_failed|gagal login/i.test(msg)) stats.login_failed++; + if (/session_expired|session habis/i.test(msg)) stats.session_expired++; + + if ((item.channel || ch).toLowerCase() === 'performance') { + stats.performance_count++; + if (item.context?.total_duration_ms) { + totalRespMs += Number(item.context.total_duration_ms); + respCount++; + } + } + + logs.push(item); + }; + + const filterSource = filterOpts.source || 'auto'; + + // 1. Forensic Logs + if (filterSource === 'auto' || filterSource === 'active') { + const forensicDir = path.join(logsRootDir, 'forensic', year, month); + if (fs.existsSync(forensicDir)) { + const files = fs.readdirSync(forensicDir); + for (const file of files) { + if (file.endsWith('.log') && file.includes(filterDate)) { + const chName = file.split('_')[0] || 'forensic'; + const content = fs.readFileSync(path.join(forensicDir, file), 'utf-8'); + content.split(/\r?\n/).forEach((l) => processLine(l, chName)); + } + } + } + } + + // 2. Activity Logs + if (filterSource === 'auto' || filterSource === 'active') { + const activityPath = path.join(logsRootDir, 'activity', year, month, `${dmY}.log`); + if (fs.existsSync(activityPath)) { + const content = fs.readFileSync(activityPath, 'utf-8'); + content.split(/\r?\n/).forEach((l) => processLine(l, 'activity')); + } + } + + // 3. Access Logs + if (filterSource === 'auto' || filterSource === 'active') { + const accessDir = path.join(logsRootDir, 'access', year, month, day); + if (fs.existsSync(accessDir)) { + const files = fs.readdirSync(accessDir); + for (const file of files) { + if (file.endsWith('.log')) { + const content = fs.readFileSync(path.join(accessDir, file), 'utf-8'); + content.split(/\r?\n/).forEach((l) => processLine(l, 'access')); + } + } + } + } + + // 4. Retention Log + if (filterSource === 'auto' || filterSource === 'active') { + const retentionPath = path.join(logsRootDir, 'retention.log'); + if (fs.existsSync(retentionPath)) { + const content = fs.readFileSync(retentionPath, 'utf-8'); + content.split(/\r?\n/).forEach((l) => { + if (!filterDate || l.includes(filterDate)) { + processLine(l, 'system'); + } + }); + } + } + + // 5. ZIP Archives (Only if explicitly selected or auto returns empty) + const backupDir = path.join(logsRootDir, 'backup'); + let zipFilesToProcess = []; + if (fs.existsSync(backupDir)) { + if (filterSource.endsWith('.zip') && fs.existsSync(path.join(backupDir, filterSource))) { + zipFilesToProcess.push(path.join(backupDir, filterSource)); + } else if (filterSource === 'all_zip' || (filterSource === 'auto' && logs.length === 0)) { + const files = fs.readdirSync(backupDir); + const ymMonth = `${year}-${month}`; + for (const file of files) { + if (file.endsWith('.zip')) { + if (filterSource === 'auto' && filterDate) { + if (file.includes(ymMonth)) { + zipFilesToProcess.push(path.join(backupDir, file)); + } + } else { + zipFilesToProcess.push(path.join(backupDir, file)); + } + } + } + } + } + + for (const zipPath of zipFilesToProcess) { + try { + const zip = new AdmZip(zipPath); + const entries = zip.getEntries(); + for (const entry of entries) { + if (entry.isDirectory) continue; + const entryName = entry.entryName; + if (filterSource === 'auto' && filterDate) { + if (!entryName.includes(filterDate) && !entryName.includes(dmY) && !entryName.includes(`/${day}/`)) { + continue; + } + } + + let ch = 'archive'; + if (entryName.includes('session')) ch = 'session'; + else if (entryName.includes('auth')) ch = 'auth'; + else if (entryName.includes('error')) ch = 'error'; + else if (entryName.includes('performance')) ch = 'performance'; + else if (entryName.includes('security')) ch = 'security'; + else if (entryName.includes('access')) ch = 'access'; + else if (entryName.includes('activity')) ch = 'activity'; + + const content = entry.getData().toString('utf-8'); + content.split(/\r?\n/).forEach((l) => processLine(l, ch)); + } + } catch (err) { + console.error(`Error reading zip file ${zipPath}:`, err); + } + } + + if (respCount > 0) { + stats.avg_response_ms = Number((totalRespMs / respCount).toFixed(2)); + } + + // Fast direct string timestamp comparison (100x faster than localeCompare/Date) + logs.sort((a, b) => { + const tsA = a.timestamp || ''; + const tsB = b.timestamp || ''; + if (tsA !== tsB) return tsB > tsA ? 1 : -1; + return (b._seq || 0) - (a._seq || 0); + }); + + // Cap browser payload but keep full stats + const trimmed = logs.length > MAX_BROWSER_ITEMS ? logs.slice(0, MAX_BROWSER_ITEMS) : logs; + stats.total_scanned = logs.length; + + return { logs: trimmed, stats }; +} + +export function getBruteForceLocks(logsRootDir) { + const bfDir = path.join(logsRootDir, 'forensic', 'cache'); + const lockedIPs = []; + if (fs.existsSync(bfDir)) { + const files = fs.readdirSync(bfDir); + const now = Math.floor(Date.now() / 1000); + for (const file of files) { + if (file.startsWith('bf_') && file.endsWith('.json')) { + try { + const raw = fs.readFileSync(path.join(bfDir, file), 'utf-8'); + const data = JSON.parse(raw); + if (data && data.lockout_until && data.lockout_until > now) { + lockedIPs.push(data); + } + } catch (e) { + // ignore invalid json + } + } + } + } + return lockedIPs; +} + +export function unblockIp(logsRootDir, targetIp) { + const bfDir = path.join(logsRootDir, 'forensic', 'cache'); + if (fs.existsSync(bfDir)) { + const files = fs.readdirSync(bfDir); + for (const file of files) { + if (file.startsWith('bf_') && file.endsWith('.json')) { + try { + const filePath = path.join(bfDir, file); + const raw = fs.readFileSync(filePath, 'utf-8'); + const data = JSON.parse(raw); + if (data && (data.ip === targetIp || file.includes(targetIp))) { + fs.unlinkSync(filePath); + return true; + } + } catch (e) { + // ignore + } + } + } + } + return false; +} + +export function getBackupArchives(logsRootDir) { + const backupDir = path.join(logsRootDir, 'backup'); + const backups = []; + if (fs.existsSync(backupDir)) { + const files = fs.readdirSync(backupDir); + for (const file of files) { + if (file.endsWith('.zip')) { + const filePath = path.join(backupDir, file); + const stat = fs.statSync(filePath); + backups.push({ + filename: file, + size_bytes: stat.size, + size_formatted: (stat.size / (1024 * 1024)).toFixed(2) + ' MB', + modified_time: new Date(stat.mtime).toISOString().replace('T', ' ').substring(0, 19), + }); + } + } + } + backups.sort((a, b) => b.filename.localeCompare(a.filename)); + return backups; +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..19236bf --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,383 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import axios from 'axios'; +import Swal from 'sweetalert2'; +import { Navbar } from './components/Navbar'; +import { LoginScreen } from './components/LoginScreen'; +import { StatsOverview } from './components/StatsOverview'; +import { BruteForcePanel } from './components/BruteForcePanel'; +import { FilterToolbar } from './components/FilterToolbar'; +import { LogTable } from './components/LogTable'; +import { BackupArchives } from './components/BackupArchives'; +import { LogEntry, LogStats, LockedIP, BackupFile, UserSession, FilterState } from './types'; +import { ListFilter, Archive, Info, Loader2 } from 'lucide-react'; + +export function App() { + const [user, setUser] = useState(null); + const [authConfig, setAuthConfig] = useState({}); + const [authLoading, setAuthLoading] = useState(true); + + const [activeTab, setActiveTab] = useState<'logs' | 'backup'>('logs'); + const [loading, setLoading] = useState(false); + const [actionMessage, setActionMessage] = useState(''); + + const [logs, setLogs] = useState([]); + const [stats, setStats] = useState({ + total: 0, + login_success: 0, + login_failed: 0, + session_expired: 0, + errors: 0, + warnings: 0, + performance_count: 0, + avg_response_ms: 0, + brute_force_locked: 0, + }); + + const [lockedIPs, setLockedIPs] = useState([]); + const [backups, setBackups] = useState([]); + const [pagination, setPagination] = useState({ + totalLogs: 0, + perPage: 100, + currentPage: 1, + totalPages: 1, + offset: 0, + }); + + const [filters, setFilters] = useState({ + source: 'auto', + date: new Date().toISOString().substring(0, 10), + time_start: '', + time_end: '', + channel: 'all', + level: 'all', + search: '', + limit: 100, + page: 1, + }); + + // Check Auth State + const checkAuth = useCallback(async () => { + setAuthLoading(true); + try { + const res = await axios.get('/api/auth/me'); + if (res.data.authenticated) { + setUser(res.data.user); + } else { + setUser(null); + } + setAuthConfig(res.data.config || {}); + } catch (e) { + console.error('Failed to fetch auth state', e); + setUser(null); + } finally { + setAuthLoading(false); + } + }, []); + + // Fetch Logs Data with SweetAlert2 Loading Indicator + const fetchLogs = useCallback(async () => { + if (!user) return; + setLoading(true); + + Swal.fire({ + title: 'Memuat Data Log Forensic...', + html: 'Sedang memindai dan mengurai data log dari server SIMRS...', + allowOutsideClick: false, + allowEscapeKey: false, + showConfirmButton: false, + didOpen: () => { + Swal.showLoading(); + }, + background: '#0f172a', + color: '#e2e8f0', + customClass: { + popup: 'border border-slate-800 rounded-2xl shadow-2xl backdrop-blur-xl', + title: 'text-cyan-400 font-bold text-sm', + htmlContainer: 'text-slate-400 text-xs mt-1', + }, + }); + + try { + const params = new URLSearchParams({ + source: filters.source, + date: filters.date, + channel: filters.channel, + level: filters.level, + time_start: filters.time_start, + time_end: filters.time_end, + search: filters.search, + page: String(filters.page), + limit: String(filters.limit), + }); + + const res = await axios.get(`/api/logs?${params.toString()}`); + const data = res.data; + + setLogs(data.logs || []); + setStats(data.stats || {}); + setLockedIPs(data.lockedIPs || []); + setBackups(data.backups || []); + if (data.pagination) { + setPagination({ + totalLogs: data.pagination.totalLogs, + perPage: data.pagination.perPage, + currentPage: data.pagination.currentPage, + totalPages: data.pagination.totalPages, + offset: data.pagination.offset, + }); + } + Swal.close(); + } catch (err: any) { + console.error('Failed to fetch logs', err); + Swal.fire({ + icon: 'error', + title: 'Gagal Memuat Log', + text: err.response?.data?.error || err.message || 'Terjadi kesalahan saat memproses data log.', + background: '#0f172a', + color: '#e2e8f0', + confirmButtonColor: '#0e7490', + customClass: { + popup: 'border border-slate-800 rounded-2xl shadow-2xl', + title: 'text-rose-400 font-bold text-sm', + }, + }); + } finally { + setLoading(false); + } + }, [filters, user]); + + useEffect(() => { + checkAuth(); + }, [checkAuth]); + + useEffect(() => { + if (user) { + fetchLogs(); + } + }, [fetchLogs, user]); + + // Handlers + const handleLogin = async (credentials: any) => { + const res = await axios.post('/api/auth/login', credentials); + if (res.data.success) { + setUser(res.data.user); + } else { + throw new Error(res.data.error || 'Login gagal.'); + } + }; + + const handleLogout = async () => { + await axios.post('/api/auth/logout'); + setUser(null); + }; + + const handleUnblock = async (ip: string) => { + try { + const res = await axios.post('/api/brute-force/unblock', { target_ip: ip }); + Swal.fire({ + icon: 'success', + title: 'Unblock Berhasil', + text: res.data.message || `IP ${ip} berhasil dibuka blokirnya.`, + timer: 2000, + showConfirmButton: false, + background: '#0f172a', + color: '#e2e8f0', + }); + fetchLogs(); + } catch (err: any) { + Swal.fire({ + icon: 'error', + title: 'Gagal Unblock', + text: err.response?.data?.error || 'Gagal unblock IP.', + background: '#0f172a', + color: '#e2e8f0', + }); + } + }; + + const handleRunRetention = async () => { + try { + Swal.fire({ + title: 'Jalankan Job Retention & Backup', + text: 'Apakah Anda yakin ingin memicu job pembersihan & pencadangan log manual?', + icon: 'question', + showCancelButton: true, + confirmButtonText: 'Ya, Jalankan', + cancelButtonText: 'Batal', + confirmButtonColor: '#0e7490', + cancelButtonColor: '#334155', + background: '#0f172a', + color: '#e2e8f0', + }).then(async (result) => { + if (result.isConfirmed) { + const res = await axios.post('/api/retention/run'); + Swal.fire({ + icon: 'success', + title: 'Job Retention Berhasil', + text: res.data.message || 'Retention job berhasil dijalankan.', + timer: 2500, + showConfirmButton: false, + background: '#0f172a', + color: '#e2e8f0', + }); + fetchLogs(); + } + }); + } catch (err: any) { + Swal.fire({ + icon: 'error', + title: 'Gagal Retention', + text: 'Gagal menjalankan retention job.', + background: '#0f172a', + color: '#e2e8f0', + }); + } + }; + + const handleExport = (type: 'csv' | 'json') => { + const params = new URLSearchParams({ + type, + source: filters.source, + date: filters.date, + channel: filters.channel, + level: filters.level, + search: filters.search, + }); + window.open(`/api/export?${params.toString()}`, '_blank'); + }; + + // Keyboard Shortcuts Listener + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ( + (e.ctrlKey && e.altKey && (e.key === 'f' || e.key === 'F')) || + (e.ctrlKey && e.shiftKey && (e.key === 'F' || e.key === 'f')) + ) { + e.preventDefault(); + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, []); + + // 1. Initial Loading State + if (authLoading) { + return ( +
+
+ +

Memeriksa Sesi Autentikasi...

+
+
+ ); + } + + // 2. MANDATORY LOGIN CHECK: If not logged in, show full-screen LoginScreen exclusively! + if (!user) { + return ; + } + + // 3. FULLY RESPONSIVE DASHBOARD (Only accessible when logged in) + return ( +
+ {}} onLogout={handleLogout} /> + +
+ {actionMessage && ( +
+
+ + {actionMessage} +
+ +
+ )} + + {/* Stats Section */} + + + {/* Brute Force Active Alerts */} + + + {/* Filter Toolbar */} + setFilters((prev) => ({ ...prev, ...updated }))} + onReset={() => + setFilters({ + source: 'auto', + date: new Date().toISOString().substring(0, 10), + time_start: '', + time_end: '', + channel: 'all', + level: 'all', + search: '', + limit: 100, + page: 1, + }) + } + /> + + {/* Main Tabbed Layout */} +
+
+ + +
+ + {activeTab === 'logs' ? ( + setFilters((prev) => ({ ...prev, page }))} + onLimitChange={(limit) => setFilters((prev) => ({ ...prev, limit, page: 1 }))} + onLoadMore={() => setFilters((prev) => ({ ...prev, limit: prev.limit + 100 }))} + onExport={handleExport} + /> + ) : ( + { + setFilters((prev) => ({ ...prev, source: filename, page: 1 })); + setActiveTab('logs'); + }} + onRunRetention={handleRunRetention} + /> + )} +
+
+
+ ); +} + +export default App; diff --git a/src/components/BackupArchives.tsx b/src/components/BackupArchives.tsx new file mode 100644 index 0000000..02e1e66 --- /dev/null +++ b/src/components/BackupArchives.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { Archive, ShieldCheck, Eye, RefreshCw } from 'lucide-react'; +import { BackupFile } from '../types'; + +interface BackupArchivesProps { + backups: BackupFile[]; + onSelectBackup: (filename: string) => void; + onRunRetention: () => void; +} + +export const BackupArchives: React.FC = ({ backups, onSelectBackup, onRunRetention }) => { + return ( +
+
+
+ + Berkas log berusia >30 hari diarsipkan secara kompresi per bulan. +
+ +
+ + {backups.length === 0 ? ( +
+ + Belum ada file arsip log zip di folder logs/backup/. File log berusia >30 hari akan diarsipkan otomatis. +
+ ) : ( +
+ + + + + + + + + + + {backups.map((bk, idx) => ( + + + + + + + ))} + +
Nama Berkas ArsipUkuran FileWaktu PembuatanAksi & Status
+ + {bk.filename} + {bk.size_mb} MB{bk.mtime} + + + Safe Archive + +
+
+ )} +
+ ); +}; diff --git a/src/components/BruteForcePanel.tsx b/src/components/BruteForcePanel.tsx new file mode 100644 index 0000000..1544479 --- /dev/null +++ b/src/components/BruteForcePanel.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { ShieldAlert, Unlock } from 'lucide-react'; +import { LockedIP } from '../types'; + +interface BruteForcePanelProps { + lockedIPs: LockedIP[]; + onUnblock: (ip: string) => void; +} + +export const BruteForcePanel: React.FC = ({ lockedIPs, onUnblock }) => { + if (lockedIPs.length === 0) return null; + + const now = Math.floor(Date.now() / 1000); + + return ( +
+
+
+ + Peringatan Keamanan: IP Terblokir (Brute Force Lockout Detected) +
+ + {lockedIPs.length} IP + +
+ +
+ + + + + + + + + + + {lockedIPs.map((item, idx) => { + const remainSeconds = Math.max(0, item.lockout_until - now); + const remainMinutes = Math.ceil(remainSeconds / 60); + return ( + + + + + + + ); + })} + +
IP AddressPercobaan GagalSisa Waktu LockoutAksi
{item.ip}{item.attempts?.length || 0}x Percobaan + {remainMinutes} Menit ({new Date(item.lockout_until * 1000).toLocaleTimeString()}) + + +
+
+
+ ); +}; diff --git a/src/components/FilterToolbar.tsx b/src/components/FilterToolbar.tsx new file mode 100644 index 0000000..f5a867b --- /dev/null +++ b/src/components/FilterToolbar.tsx @@ -0,0 +1,158 @@ +import React from 'react'; +import { Database, Calendar, Clock, Layers, AlertCircle, Search, Filter, RotateCcw } from 'lucide-react'; +import { FilterState, BackupFile } from '../types'; + +interface FilterToolbarProps { + filters: FilterState; + backups: BackupFile[]; + onChange: (updated: Partial) => void; + onReset: () => void; +} + +export const FilterToolbar: React.FC = ({ filters, backups, onChange, onReset }) => { + return ( +
+
{ + e.preventDefault(); + onChange({ page: 1 }); + }} + className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-7 gap-3 items-end" + > + {/* Sumber Log */} +
+ + +
+ + {/* Tanggal */} +
+ + onChange({ date: e.target.value, page: 1 })} + className="w-full bg-slate-950 border border-slate-800 rounded-lg px-2.5 py-1.5 text-xs text-white focus:border-cyan-500 outline-none" + /> +
+ + {/* Rentang Jam */} +
+ +
+ onChange({ time_start: e.target.value, page: 1 })} + className="w-full bg-slate-950 border border-slate-800 rounded-lg px-1.5 py-1.5 text-[11px] text-white focus:border-cyan-500 outline-none" + /> + - + onChange({ time_end: e.target.value, page: 1 })} + className="w-full bg-slate-950 border border-slate-800 rounded-lg px-1.5 py-1.5 text-[11px] text-white focus:border-cyan-500 outline-none" + /> +
+
+ + {/* Channel */} +
+ + +
+ + {/* Level */} +
+ + +
+ + {/* Kata Kunci */} +
+ + onChange({ search: e.target.value })} + className="w-full bg-slate-950 border border-slate-800 rounded-lg px-2.5 py-1.5 text-xs text-white focus:border-cyan-500 outline-none" + /> +
+ + {/* Action Buttons */} +
+ + +
+
+
+ ); +}; diff --git a/src/components/LogTable.tsx b/src/components/LogTable.tsx new file mode 100644 index 0000000..7c7403e --- /dev/null +++ b/src/components/LogTable.tsx @@ -0,0 +1,320 @@ +import React, { useState } from 'react'; +import { Code, Copy, Check, Eye, EyeOff, FileText, Download, ChevronLeft, ChevronRight, PlusCircle, Link as LinkIcon } from 'lucide-react'; +import { LogEntry } from '../types'; + +interface LogTableProps { + logs: LogEntry[]; + totalLogs: number; + currentPage: number; + totalPages: number; + limit: number; + offset: number; + loading: boolean; + onPageChange: (page: number) => void; + onLimitChange: (limit: number) => void; + onLoadMore: () => void; + onExport: (type: 'csv' | 'json') => void; +} + +export const LogTable: React.FC = ({ + logs, + totalLogs, + currentPage, + totalPages, + limit, + offset, + loading, + onPageChange, + onLimitChange, + onLoadMore, + onExport, +}) => { + const [expandedRows, setExpandedRows] = useState>({}); + const [expandedMsgs, setExpandedMsgs] = useState>({}); + const [copiedRow, setCopiedRow] = useState(null); + + const toggleRow = (idx: number) => { + setExpandedRows((prev) => ({ ...prev, [idx]: !prev[idx] })); + }; + + const toggleMsg = (idx: number) => { + setExpandedMsgs((prev) => ({ ...prev, [idx]: !prev[idx] })); + }; + + const fallbackCopyText = (text: string, idx: number) => { + try { + const textArea = document.createElement('textarea'); + textArea.value = text; + textArea.style.position = 'fixed'; + textArea.style.top = '0'; + textArea.style.left = '0'; + textArea.style.opacity = '0'; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + const successful = document.execCommand('copy'); + document.body.removeChild(textArea); + if (successful) { + setCopiedRow(idx); + setTimeout(() => setCopiedRow(null), 2000); + } + } catch (err) { + console.error('Gagal menyalin JSON:', err); + } + }; + + const handleCopyJson = (log: LogEntry, idx: number) => { + const jsonStr = JSON.stringify(log, null, 2); + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard + .writeText(jsonStr) + .then(() => { + setCopiedRow(idx); + setTimeout(() => setCopiedRow(null), 2000); + }) + .catch(() => { + fallbackCopyText(jsonStr, idx); + }); + } else { + fallbackCopyText(jsonStr, idx); + } + }; + + const getLevelBadge = (level: string) => { + const lvl = (level || 'INFO').toUpperCase(); + switch (lvl) { + case 'CRITICAL': + return 'bg-rose-600 text-white font-bold'; + case 'ERROR': + return 'bg-orange-500 text-white font-bold'; + case 'WARNING': + return 'bg-amber-400 text-slate-950 font-bold'; + case 'DEBUG': + return 'bg-slate-600 text-white'; + default: + return 'bg-cyan-600 text-white'; + } + }; + + return ( +
+ {/* Header Bar */} +
+
+ Menampilkan {totalLogs > 0 ? offset + 1 : 0} - {Math.min(offset + logs.length, totalLogs)} dari{' '} + {totalLogs.toLocaleString()} baris (Halaman {currentPage} dari {totalPages}) +
+ +
+
+ Limit: + +
+ + + +
+
+ + {/* Table Container */} +
+ + + + + + + + + + + + + + {logs.length === 0 ? ( + + + + ) : ( + logs.map((log, idx) => { + const isMsgLong = (log.message || '').length > 130; + const isMsgExpanded = expandedMsgs[idx]; + const isRowExpanded = expandedRows[idx]; + + return ( + + + + + + + + + + + + {/* Expandable JSON detail row */} + {isRowExpanded && ( + + + + )} + + ); + }) + )} + +
WaktuLevelChannelIP AddressUser NIPPesan / EventDetail
+ + Tidak ada log ditemukan untuk kriteria filter ini. +
+
{log.timestamp ? log.timestamp.substring(0, 10) : '-'}
+
{log.timestamp ? log.timestamp.substring(11, 19) : '-'}
+
+ + {log.level || 'INFO'} + + + + {log.channel || '-'} + + + {log.context?.ip_address || '-'} + + {log.context?.user_nip || '-'} + +
+ {isMsgLong && !isMsgExpanded ? ( + + {log.message.substring(0, 130)}...{' '} + + + ) : ( + + {log.message} + {isMsgLong && ( + + )} + + )} +
+ + {log.context?.request_uri && ( +
+ + {log.context.request_uri} +
+ )} +
+ +
+
+ + Data Log JSON Structure (Row #{offset + idx + 1}) + + +
+
+
+                              {JSON.stringify(log, null, 2)}
+                            
+
+
+
+ + {/* Pagination Footer */} + {totalLogs > 0 && ( +
+
+ Halaman {currentPage} dari {totalPages} +
+ +
+ + + + {currentPage} / {totalPages} + + + +
+ + {currentPage < totalPages && ( + + )} +
+ )} +
+ ); +}; diff --git a/src/components/LoginModal.tsx b/src/components/LoginModal.tsx new file mode 100644 index 0000000..9ec475e --- /dev/null +++ b/src/components/LoginModal.tsx @@ -0,0 +1,186 @@ +import React, { useState } from 'react'; +import { Key, User, Lock, Settings, AlertTriangle, Eye, EyeOff, X } from 'lucide-react'; + +interface LoginModalProps { + isOpen: boolean; + onClose: () => void; + onLogin: (credentials: any) => Promise; + defaultConfig?: any; +} + +export const LoginModal: React.FC = ({ + isOpen, + onClose, + onLogin, + defaultConfig = {}, +}) => { + const [username, setUsername] = useState('stim@rssa.id'); + const [password, setPassword] = useState('RSSAjaya2026'); + const [showPassword, setShowPassword] = useState(false); + const [showAdvanced, setShowAdvanced] = useState(false); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + const [kcServer, setKcServer] = useState(defaultConfig.defaultKcServer || 'https://auth.rssa.top/'); + const [kcRealm, setKcRealm] = useState(defaultConfig.defaultKcRealm || 'rssa'); + const [kcClientId, setKcClientId] = useState(defaultConfig.defaultKcClient || 'satu'); + const [kcClientSecret, setKcClientSecret] = useState(defaultConfig.defaultKcSecret || 'ZhkK45MHB0a0eAZX5ecNTnlfnWlZXfBE'); + + if (!isOpen) return null; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setLoading(true); + + try { + await onLogin({ + username, + password, + kc_server: kcServer, + kc_realm: kcRealm, + kc_client_id: kcClientId, + kc_client_secret: kcClientSecret, + }); + onClose(); + } catch (err: any) { + setError(err.message || 'Login gagal. Periksa username dan password Anda.'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ + +
+
+ +
+

SIMRS Forensic Inspector

+

Masuk menggunakan Keycloak SSO / Local Login

+
+ + {error && ( +
+ + {error} +
+ )} + +
+
+ +
+
+ +
+ setUsername(e.target.value)} + placeholder="stim@rssa.id" + required + className="w-full pl-9 pr-3 py-2 rounded-lg bg-slate-950 border border-slate-800 text-white text-sm focus:border-cyan-500 focus:ring-1 focus:ring-cyan-500 transition-all outline-none" + /> +
+
+ +
+ +
+
+ +
+ setPassword(e.target.value)} + placeholder="Masukkan Password" + required + className="w-full pl-9 pr-10 py-2 rounded-lg bg-slate-950 border border-slate-800 text-white text-sm focus:border-cyan-500 focus:ring-1 focus:ring-cyan-500 transition-all outline-none" + /> + +
+
+ + {/* Advanced Keycloak Server Accordion */} +
+ + + {showAdvanced && ( +
+
+ + setKcServer(e.target.value)} + className="w-full px-2.5 py-1.5 rounded bg-slate-900 border border-slate-800 text-white text-xs" + /> +
+
+
+ + setKcRealm(e.target.value)} + className="w-full px-2.5 py-1.5 rounded bg-slate-900 border border-slate-800 text-white text-xs" + /> +
+
+ + setKcClientId(e.target.value)} + className="w-full px-2.5 py-1.5 rounded bg-slate-900 border border-slate-800 text-white text-xs" + /> +
+
+
+ + setKcClientSecret(e.target.value)} + className="w-full px-2.5 py-1.5 rounded bg-slate-900 border border-slate-800 text-white text-xs" + /> +
+
+ )} +
+ + +
+
+
+ ); +}; diff --git a/src/components/LoginScreen.tsx b/src/components/LoginScreen.tsx new file mode 100644 index 0000000..02cb024 --- /dev/null +++ b/src/components/LoginScreen.tsx @@ -0,0 +1,266 @@ +import React, { useState, useEffect } from 'react'; +import { Key, User, Lock, Settings, AlertTriangle, Eye, EyeOff, Keyboard, ArrowLeft, ExternalLink, ShieldCheck } from 'lucide-react'; + +interface LoginScreenProps { + onLogin: (credentials: any) => Promise; + defaultConfig?: any; +} + +export const LoginScreen: React.FC = ({ onLogin, defaultConfig = {} }) => { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [showAdvanced, setShowAdvanced] = useState(false); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + const isKcEnvConfigured = defaultConfig?.isKcEnvConfigured ?? false; + + const [kcServer, setKcServer] = useState(defaultConfig.defaultKcServer || 'https://auth.rssa.top/'); + const [kcRealm, setKcRealm] = useState(defaultConfig.defaultKcRealm || 'rssa'); + const [kcClientId, setKcClientId] = useState(defaultConfig.defaultKcClient || 'satu'); + const [kcClientSecret, setKcClientSecret] = useState(defaultConfig.defaultKcSecret || 'ZhkK45MHB0a0eAZX5ecNTnlfnWlZXfBE'); + const [kcRedirectUri, setKcRedirectUri] = useState(''); + + // Check URL query error + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const errParam = params.get('error'); + if (errParam) { + setError(decodeURIComponent(errParam)); + } + }, []); + + const handlePkceLogin = () => { + const params = new URLSearchParams({ + kc_server: kcServer, + kc_realm: kcRealm, + kc_client_id: kcClientId, + kc_client_secret: kcClientSecret, + }); + if (kcRedirectUri) { + params.append('kc_redirect_uri', kcRedirectUri); + } + window.location.href = `/api/auth/pkce/login?${params.toString()}`; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setLoading(true); + + try { + await onLogin({ + username, + password, + kc_server: kcServer, + kc_realm: kcRealm, + kc_client_id: kcClientId, + kc_client_secret: kcClientSecret, + }); + } catch (err: any) { + setError(err.message || 'Login gagal. Periksa username dan password.'); + } finally { + setLoading(false); + } + }; + + const simrsUrl = defaultConfig?.simrsUrl || '/'; + + return ( +
+ {/* Ambient Glows */} +
+
+ +
+ {/* Brand Icon */} +
+ +
+ +
+

SIMRS Logs Inspector

+

+ + Keamanan SSO Terproteksi Standar +

+
+ + {error && ( +
+ + {error} +
+ )} + + {/* Primary PKCE Login Action */} +
+ +

+ Direkomendasikan. Pengalihan aman ke server identitas Keycloak. +

+
+ +
+
+ Atau Akun Lokal / Pengujian +
+
+ + {/* Local / Direct Fallback Login Form */} +
+
+ +
+
+ +
+ setUsername(e.target.value)} + placeholder="Username / Email (cth: stim@rssa.id)" + className="w-full pl-10 pr-3.5 py-2 rounded-xl bg-slate-950/80 border border-slate-800 text-white text-sm focus:border-cyan-500 focus:ring-1 focus:ring-cyan-500 transition-all outline-none" + /> +
+
+ +
+ +
+
+ +
+ setPassword(e.target.value)} + placeholder="Masukkan Password" + className="w-full pl-10 pr-10 py-2 rounded-xl bg-slate-950/80 border border-slate-800 text-white text-sm focus:border-cyan-500 focus:ring-1 focus:ring-cyan-500 transition-all outline-none" + /> + +
+
+ + {/* Advanced Keycloak Server Config (Ditampilkan HANYA jika .env Keycloak belum terkonfigurasi) */} + {!isKcEnvConfigured && ( +
+ + + {showAdvanced && ( +
+
+ + setKcServer(e.target.value)} + className="w-full px-3 py-1.5 rounded-lg bg-slate-900 border border-slate-800 text-white text-xs" + /> +
+
+
+ + setKcRealm(e.target.value)} + className="w-full px-3 py-1.5 rounded-lg bg-slate-900 border border-slate-800 text-white text-xs" + /> +
+
+ + setKcClientId(e.target.value)} + className="w-full px-3 py-1.5 rounded-lg bg-slate-900 border border-slate-800 text-white text-xs" + /> +
+
+
+ + setKcClientSecret(e.target.value)} + className="w-full px-3 py-1.5 rounded-lg bg-slate-900 border border-slate-800 text-white text-xs" + /> +
+
+ + setKcRedirectUri(e.target.value)} + placeholder="http://meninjar.dev.rssa.id:5880/api/auth/pkce/callback" + className="w-full px-3 py-1.5 rounded-lg bg-slate-900 border border-slate-800 text-white text-xs font-mono" + /> +
+
+ )} +
+ )} + + +
+ + {/* Secret Hotkey Hint */} + {/*
+
+ alert( + '🔑 Secret Key Shortcuts:\n\n1. [Ctrl + Alt + F] atau [Ctrl + Shift + F] : Langsung menuju Forensic Inspector\n2. [Ctrl + Shift + K] : Fokus cepat ke kolom Username SSO\n3. Ketik kata "forensic" atau "inspector" kapan saja.' + ) + } + className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-slate-950 border border-slate-800 text-cyan-400 text-xs font-mono cursor-pointer hover:border-cyan-500/50 transition-colors" + > + + Shortcut: Ctrl+Alt+F / forensic +
+
*/} + + +
+
+ ); +}; diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx new file mode 100644 index 0000000..65d3ee5 --- /dev/null +++ b/src/components/Navbar.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { Search, UserCheck, Power, Key, Home } from 'lucide-react'; +import { UserSession } from '../types'; + +interface NavbarProps { + user: UserSession | null; + simrsUrl?: string; + onOpenLogin: () => void; + onLogout: () => void; +} + +export const Navbar: React.FC = ({ user, simrsUrl = '/', onLogout }) => { + const handlePkceLogin = () => { + window.location.href = '/api/auth/pkce/login'; + }; + + return ( +
+
+ {/* Brand */} +
+
+ +
+
+

+ SIMRS Logs Inspector +

+

System Diagnostics, Audit & Security Monitor

+
+
+ + {/* Right Actions */} +
+ {user ? ( +
+
+ + SSO ({user.auth_type?.includes('pkce') ? 'PKCE' : 'Local'}): {user.name || user.username} +
+ +
+ ) : ( + + )} + + + + Ke SIMRS + +
+
+
+ ); +}; diff --git a/src/components/StatsOverview.tsx b/src/components/StatsOverview.tsx new file mode 100644 index 0000000..7b9709b --- /dev/null +++ b/src/components/StatsOverview.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { Activity, CheckCircle, Clock, AlertOctagon, Zap, ShieldAlert } from 'lucide-react'; +import { LogStats } from '../types'; + +interface StatsOverviewProps { + stats: LogStats; + selectedDate: string; +} + +export const StatsOverview: React.FC = ({ stats, selectedDate }) => { + return ( +
+ {/* Total Event */} +
+
+ Total Event + +
+
+
{stats.total.toLocaleString()}
+
{selectedDate}
+
+
+ + {/* Login Sukses */} +
+
+ Login Sukses + +
+
+
{stats.login_success.toLocaleString()}
+
+ Gagal: {stats.login_failed} +
+
+
+ + {/* Session Expired */} +
+
+ Session Expired + +
+
+
{stats.session_expired.toLocaleString()}
+
Navigasi Terhalang
+
+
+ + {/* Errors & Critical */} +
+
+ Errors / Crit + +
+
+
{stats.errors.toLocaleString()}
+
+ Warning: {stats.warnings} +
+
+
+ + {/* Avg Response Time */} +
+
+ Avg Response + +
+
+
+ {stats.avg_response_ms} ms +
+
{stats.performance_count} Request
+
+
+ + {/* Brute Force Locked */} +
+
+ IP Locked + +
+
+
{stats.brute_force_locked}
+
Brute Force Detected
+
+
+
+ ); +}; diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..0db7c60 --- /dev/null +++ b/src/index.css @@ -0,0 +1,51 @@ +@import "tailwindcss"; + +@layer base { + html, body, #root { + background-color: #090d16; + color: #e2e8f0; + font-family: 'Inter', system-ui, -apple-system, sans-serif; + overflow-x: hidden; + max-width: 100vw; + } +} + +.glass-card { + background: rgba(30, 41, 59, 0.7); + backdrop-filter: blur(12px); + border: 1px solid rgba(51, 65, 85, 0.6); + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4); +} + +.glass-card-hover { + transition: all 0.2s ease-in-out; +} +.glass-card-hover:hover { + transform: translateY(-2px); + border-color: rgba(56, 189, 248, 0.4); + box-shadow: 0 15px 30px -5px rgba(14, 165, 233, 0.15); +} + +.json-box { + background: #050811; + border: 1px solid #1e293b; + color: #38bdf8; + font-family: 'Fira Code', monospace; + font-size: 0.82rem; +} + +/* Custom scrollbars */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} +::-webkit-scrollbar-track { + background: #090d16; +} +::-webkit-scrollbar-thumb { + background: #334155; + border-radius: 4px; +} +::-webkit-scrollbar-thumb:hover { + background: #475569; +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..93db379 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App.tsx'; +import './index.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..78593cd --- /dev/null +++ b/src/types.ts @@ -0,0 +1,64 @@ +export interface LogContext { + ip_address?: string; + user_nip?: string; + request_uri?: string; + total_duration_ms?: number; + raw_log?: string; + [key: string]: any; +} + +export interface LogEntry { + timestamp: string; + channel: string; + level: string; + message: string; + correlation_id: string; + context: LogContext; + _seq?: number; +} + +export interface LogStats { + total: number; + login_success: number; + login_failed: number; + session_expired: number; + errors: number; + warnings: number; + performance_count: number; + avg_response_ms: number; + brute_force_locked: number; +} + +export interface LockedIP { + ip: string; + attempts: any[]; + lockout_until: number; +} + +export interface BackupFile { + filename: string; + size_mb: number; + mtime: string; +} + +export interface UserSession { + username: string; + name: string; + email?: string; + login_time: string; + auth_type: string; + kc_server?: string; + kc_realm?: string; +} + +export interface FilterState { + source: string; + date: string; + time_start: string; + time_end: string; + channel: string; + level: string; + search: string; + limit: number; + page: number; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bab037f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..24f9cc0 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import tailwindcss from '@tailwindcss/vite'; + +export default defineConfig({ + plugins: [react(), tailwindcss()], + server: { + port: 5880, + host: true, + allowedHosts: true, + watch: { + usePolling: true, + interval: 500, + ignored: ['**/logs/**', '**/node_modules/**', '**/dist/**', '**/.git/**'], + }, + proxy: { + '/api': { + target: 'http://localhost:5880', + changeOrigin: true, + }, + }, + }, +});